Main changes to the chips library:
Changed attribute styles
- Added an avatar position (left, right) inside the chip
- Added a disableDelete boolean
- Removed chipsAlternateLayout

Changed how to layout contacts in autocomplete list.
Added a separate class to handle layout and binding information. (This way clients can extend this class and have a customizable layout)
This new class lives inside RecipientEditTextView and is passed onto each adapter.

Merged code paths for createSelectedChip and createUnselectedChip.
Merged code paths for getView() in each of the 3 adapters by using DropdownChipLayouter.

All tests still pass since this CL is mostly infrastructure layout changes.

There are some non-backwards compatible changes with this if other clients are using this library.
BaseRecipientAdapter no longer has protected methods to override the layout and id's.
The attribute file no longer has chipsAlternateLaytout.
Both these features can still be used through the new DropdownChipLayouter class.

Change-Id: I4496232eddd194be2df6a047f75637114f2eaa52
diff --git a/res/values/attrs.xml b/res/values/attrs.xml
index 44c2500..6b26bcb 100644
--- a/res/values/attrs.xml
+++ b/res/values/attrs.xml
@@ -15,13 +15,17 @@
 -->
 <resources>
     <declare-styleable name="RecipientEditTextView">
-        <attr name="invalidChipBackground" format="reference" />
+        <attr name="avatarPosition">
+            <enum name="right" value="0" />
+            <enum name="left" value="1" />
+        </attr>
         <attr name="chipBackground" format="reference" />
         <attr name="chipBackgroundPressed" format="reference" />
         <attr name="chipDelete" format="reference" />
-        <attr name="chipAlternatesLayout" format="reference" />
-        <attr name="chipPadding" format="reference" />
-        <attr name="chipHeight" format="reference" />
         <attr name="chipFontSize" format="reference" />
+        <attr name="chipHeight" format="reference" />
+        <attr name="chipPadding" format="reference" />
+        <attr name="disableDelete" format="boolean" />
+        <attr name="invalidChipBackground" format="reference" />
     </declare-styleable>
-</resources>
+</resources>
\ No newline at end of file
diff --git a/src/com/android/ex/chips/BaseRecipientAdapter.java b/src/com/android/ex/chips/BaseRecipientAdapter.java
index b779776..d52c7ca 100644
--- a/src/com/android/ex/chips/BaseRecipientAdapter.java
+++ b/src/com/android/ex/chips/BaseRecipientAdapter.java
@@ -23,8 +23,6 @@
 import android.content.pm.PackageManager.NameNotFoundException;
 import android.content.res.Resources;
 import android.database.Cursor;
-import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
 import android.net.Uri;
 import android.os.AsyncTask;
 import android.os.Handler;
@@ -43,12 +41,12 @@
 import android.widget.BaseAdapter;
 import android.widget.Filter;
 import android.widget.Filterable;
-import android.widget.ImageView;
-import android.widget.TextView;
+
+import com.android.ex.chips.DropdownChipLayouter.AdapterType;
 
 import java.io.ByteArrayOutputStream;
-import java.io.InputStream;
 import java.io.IOException;
+import java.io.InputStream;
 import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.LinkedHashMap;
@@ -460,6 +458,7 @@
     private final LayoutInflater mInflater;
     private Account mAccount;
     private final int mPreferredMaxResultCount;
+    private DropdownChipLayouter mDropdownChipLayouter;
 
     /**
      * {@link #mEntries} is responsible for showing every result for this Adapter. To
@@ -570,6 +569,15 @@
         return mQueryType;
     }
 
+    public void setDropdownChipLayouter(DropdownChipLayouter dropdownChipLayouter) {
+        mDropdownChipLayouter = dropdownChipLayouter;
+        mDropdownChipLayouter.setQuery(mQuery);
+    }
+
+    public DropdownChipLayouter getDropdownChipLayouter() {
+        return mDropdownChipLayouter;
+    }
+
     /**
      * Set the account when known. Causes the search to prioritize contacts from that account.
      */
@@ -943,111 +951,9 @@
     @Override
     public View getView(int position, View convertView, ViewGroup parent) {
         final RecipientEntry entry = getEntries().get(position);
-        String displayName = entry.getDisplayName();
-        String destination = entry.getDestination();
-        if (TextUtils.isEmpty(displayName) || TextUtils.equals(displayName, destination)) {
-            displayName = destination;
 
-            // We only show the destination for secondary entries, so clear it
-            // only for the first level.
-            if (entry.isFirstLevel()) {
-                destination = null;
-            }
-        }
-
-        final View itemView = convertView != null ? convertView : mInflater.inflate(
-                getItemLayout(), parent, false);
-        final TextView displayNameView = (TextView) itemView.findViewById(getDisplayNameId());
-        final TextView destinationView = (TextView) itemView.findViewById(getDestinationId());
-        final TextView destinationTypeView = (TextView) itemView
-                .findViewById(getDestinationTypeId());
-        final ImageView imageView = (ImageView) itemView.findViewById(getPhotoId());
-        displayNameView.setText(displayName);
-        if (!TextUtils.isEmpty(destination)) {
-            destinationView.setText(destination);
-        } else {
-            destinationView.setText(null);
-        }
-        if (destinationTypeView != null) {
-            final CharSequence destinationType = mQuery
-                    .getTypeLabel(mContext.getResources(), entry.getDestinationType(),
-                            entry.getDestinationLabel()).toString().toUpperCase();
-
-            destinationTypeView.setText(destinationType);
-        }
-
-        if (entry.isFirstLevel()) {
-            displayNameView.setVisibility(View.VISIBLE);
-            if (imageView != null) {
-                imageView.setVisibility(View.VISIBLE);
-                final byte[] photoBytes = entry.getPhotoBytes();
-                if (photoBytes != null) {
-                    final Bitmap photo = BitmapFactory.decodeByteArray(photoBytes, 0,
-                            photoBytes.length);
-                    imageView.setImageBitmap(photo);
-                } else {
-                    imageView.setImageResource(getDefaultPhotoResource());
-                }
-            }
-        } else {
-            displayNameView.setVisibility(View.GONE);
-            if (imageView != null) {
-                imageView.setVisibility(View.INVISIBLE);
-            }
-        }
-        return itemView;
-    }
-
-    /**
-     * Returns a layout id for each item inside auto-complete list.
-     *
-     * Each View must contain two TextViews (for display name and destination) and one ImageView
-     * (for photo). Ids for those should be available via {@link #getDisplayNameId()},
-     * {@link #getDestinationId()}, and {@link #getPhotoId()}.
-     */
-    protected int getItemLayout() {
-        return R.layout.chips_recipient_dropdown_item;
-    }
-
-    /**
-     * Returns a resource ID representing an image which should be shown when ther's no relevant
-     * photo is available.
-     */
-    protected int getDefaultPhotoResource() {
-        return R.drawable.ic_contact_picture;
-    }
-
-    /**
-     * Returns an id for TextView in an item View for showing a display name. By default
-     * {@link android.R.id#title} is returned.
-     */
-    protected int getDisplayNameId() {
-        return android.R.id.title;
-    }
-
-    /**
-     * Returns an id for TextView in an item View for showing a destination
-     * (an email address or a phone number).
-     * By default {@link android.R.id#text1} is returned.
-     */
-    protected int getDestinationId() {
-        return android.R.id.text1;
-    }
-
-    /**
-     * Returns an id for TextView in an item View for showing the type of the destination.
-     * By default {@link android.R.id#text2} is returned.
-     */
-    protected int getDestinationTypeId() {
-        return android.R.id.text2;
-    }
-
-    /**
-     * Returns an id for ImageView in an item View for showing photo image for a person. In default
-     * {@link android.R.id#icon} is returned.
-     */
-    protected int getPhotoId() {
-        return android.R.id.icon;
+        return mDropdownChipLayouter.bindView(convertView, parent, entry, position,
+                AdapterType.BASE_RECIPIENT, mCurrentConstraint.toString());
     }
 
     public Account getAccount() {
diff --git a/src/com/android/ex/chips/DropdownChipLayouter.java b/src/com/android/ex/chips/DropdownChipLayouter.java
new file mode 100644
index 0000000..aceefab
--- /dev/null
+++ b/src/com/android/ex/chips/DropdownChipLayouter.java
@@ -0,0 +1,274 @@
+package com.android.ex.chips;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.net.Uri;
+import android.text.TextUtils;
+import android.text.util.Rfc822Tokenizer;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import com.android.ex.chips.Queries.Query;
+
+/**
+ * A class that inflates and binds the views in the dropdown list from
+ * RecipientEditTextView.
+ */
+public class DropdownChipLayouter {
+    /**
+     * The type of adapter that is requesting a chip layout.
+     */
+    public enum AdapterType {
+        BASE_RECIPIENT,
+        RECIPIENT_ALTERNATES,
+        SINGLE_RECIPIENT
+    }
+
+    private final LayoutInflater mInflater;
+    private final Context mContext;
+    private Query mQuery;
+
+    public DropdownChipLayouter(LayoutInflater inflater, Context context) {
+        mInflater = inflater;
+        mContext = context;
+    }
+
+    public void setQuery(Query query) {
+        mQuery = query;
+    }
+
+
+    /**
+     * Layouts and binds recipient information to the view. If convertView is null, inflates a new
+     * view with getItemLaytout().
+     *
+     * @param convertView The view to bind information to.
+     * @param parent The parent to bind the view to if we inflate a new view.
+     * @param entry The recipient entry to get information from.
+     * @param position The position in the list.
+     * @param type The adapter type that is requesting the bind.
+     * @param constraint The constraint typed in the auto complete view.
+     *
+     * @return A view ready to be shown in the drop down list.
+     */
+    public View bindView(View convertView, ViewGroup parent, RecipientEntry entry, int position,
+        AdapterType type, String constraint) {
+        // Default to show all the information
+        String displayName = entry.getDisplayName();
+        String destination = entry.getDestination();
+        boolean showImage = true;
+        CharSequence destinationType = getDestinationType(entry);
+
+        final View itemView = reuseOrInflateView(convertView, parent, type);
+
+        final ViewHolder viewHolder = new ViewHolder(itemView);
+
+        // Hide some information depending on the entry type and adapter type
+        switch (type) {
+            case BASE_RECIPIENT:
+                if (TextUtils.isEmpty(displayName) || TextUtils.equals(displayName, destination)) {
+                    displayName = destination;
+
+                    // We only show the destination for secondary entries, so clear it only for the
+                    // first level.
+                    if (entry.isFirstLevel()) {
+                        destination = null;
+                    }
+                }
+
+                if (!entry.isFirstLevel()) {
+                    displayName = null;
+                    showImage = false;
+                }
+                break;
+            case RECIPIENT_ALTERNATES:
+                if (position != 0) {
+                    displayName = null;
+                    showImage = false;
+                }
+                break;
+            case SINGLE_RECIPIENT:
+                destination = Rfc822Tokenizer.tokenize(entry.getDestination())[0].getAddress();
+                destinationType = null;
+        }
+
+        // Bind the information to the view
+        bindTextToView(displayName, viewHolder.displayNameView);
+        bindTextToView(destination, viewHolder.destinationView);
+        bindTextToView(destinationType, viewHolder.destinationTypeView);
+        bindIconToView(showImage, entry, viewHolder.imageView, type);
+
+        return itemView;
+    }
+
+    /**
+     * Returns a new view with {@link #getItemLayoutResId()}.
+     */
+    public View newView() {
+        return mInflater.inflate(getItemLayoutResId(), null);
+    }
+
+    /**
+     * Returns the same view, or inflates a new one if the given view was null.
+     */
+    protected View reuseOrInflateView(View convertView, ViewGroup parent, AdapterType type) {
+        int itemLayout = getItemLayoutResId();
+        switch (type) {
+            case BASE_RECIPIENT:
+            case RECIPIENT_ALTERNATES:
+                break;
+            case SINGLE_RECIPIENT:
+                itemLayout = getAlternateItemLayoutResId();
+                break;
+        }
+        return convertView != null ? convertView : mInflater.inflate(itemLayout, parent, false);
+    }
+
+    /**
+     * Binds the text to the given text view. If the text was null, hides the text view.
+     */
+    protected void bindTextToView(CharSequence text, TextView view) {
+        if (view == null) {
+            return;
+        }
+
+        if (text != null) {
+            view.setText(text);
+            view.setVisibility(View.VISIBLE);
+        } else {
+            view.setVisibility(View.GONE);
+        }
+    }
+
+    /**
+     * Binds the avatar icon to the image view. If we don't want to show the image, hides the
+     * image view.
+     */
+    protected void bindIconToView(boolean showImage, RecipientEntry entry, ImageView view,
+        AdapterType type) {
+        if (view == null) {
+            return;
+        }
+
+        if (showImage) {
+            switch (type) {
+                case BASE_RECIPIENT:
+                    byte[] photoBytes = entry.getPhotoBytes();
+                    if (photoBytes != null && photoBytes.length > 0) {
+                        final Bitmap photo = BitmapFactory.decodeByteArray(photoBytes, 0,
+                            photoBytes.length);
+                        view.setImageBitmap(photo);
+                    } else {
+                        view.setImageResource(getDefaultPhotoResId());
+                    }
+                    break;
+                case RECIPIENT_ALTERNATES:
+                    Uri thumbnailUri = entry.getPhotoThumbnailUri();
+                    if (thumbnailUri != null) {
+                        // TODO: see if this needs to be done outside the main thread
+                        // as it may be too slow to get immediately.
+                        view.setImageURI(entry.getPhotoThumbnailUri());
+                    } else {
+                        view.setImageResource(getDefaultPhotoResId());
+                    }
+                    break;
+                case SINGLE_RECIPIENT:
+                default:
+                    break;
+            }
+            view.setVisibility(View.VISIBLE);
+        } else {
+            view.setVisibility(View.GONE);
+        }
+    }
+
+    protected CharSequence getDestinationType(RecipientEntry entry) {
+        return mQuery.getTypeLabel(mContext.getResources(), entry.getDestinationType(),
+            entry.getDestinationLabel()).toString().toUpperCase();
+    }
+
+    /**
+     * Returns a layout id for each item inside auto-complete list.
+     *
+     * Each View must contain two TextViews (for display name and destination) and one ImageView
+     * (for photo). Ids for those should be available via {@link #getDisplayNameResId()},
+     * {@link #getDestinationResId()}, and {@link #getPhotoResId()}.
+     */
+    protected int getItemLayoutResId() {
+        return R.layout.chips_recipient_dropdown_item;
+    }
+
+    /**
+     * Returns a layout id for each item inside alternate auto-complete list.
+     *
+     * Each View must contain two TextViews (for display name and destination) and one ImageView
+     * (for photo). Ids for those should be available via {@link #getDisplayNameResId()},
+     * {@link #getDestinationResId()}, and {@link #getPhotoResId()}.
+     */
+    protected int getAlternateItemLayoutResId() {
+        return R.layout.chips_alternate_item;
+    }
+
+    /**
+     * Returns a resource ID representing an image which should be shown when ther's no relevant
+     * photo is available.
+     */
+    protected int getDefaultPhotoResId() {
+        return R.drawable.ic_contact_picture;
+    }
+
+    /**
+     * Returns an id for TextView in an item View for showing a display name. By default
+     * {@link android.R.id#title} is returned.
+     */
+    protected int getDisplayNameResId() {
+        return android.R.id.title;
+    }
+
+    /**
+     * Returns an id for TextView in an item View for showing a destination
+     * (an email address or a phone number).
+     * By default {@link android.R.id#text1} is returned.
+     */
+    protected int getDestinationResId() {
+        return android.R.id.text1;
+    }
+
+    /**
+     * Returns an id for TextView in an item View for showing the type of the destination.
+     * By default {@link android.R.id#text2} is returned.
+     */
+    protected int getDestinationTypeResId() {
+        return android.R.id.text2;
+    }
+
+    /**
+     * Returns an id for ImageView in an item View for showing photo image for a person. In default
+     * {@link android.R.id#icon} is returned.
+     */
+    protected int getPhotoResId() {
+        return android.R.id.icon;
+    }
+
+    /**
+     * A holder class the view. Uses the getters in DropdownChipLayouter to find the id of the
+     * corresponding views.
+     */
+    protected class ViewHolder {
+        public final TextView displayNameView;
+        public final TextView destinationView;
+        public final TextView destinationTypeView;
+        public final ImageView imageView;
+
+        public ViewHolder(View view) {
+            displayNameView = (TextView) view.findViewById(getDisplayNameResId());
+            destinationView = (TextView) view.findViewById(getDestinationResId());
+            destinationTypeView = (TextView) view.findViewById(getDestinationTypeResId());
+            imageView = (ImageView) view.findViewById(getPhotoResId());
+        }
+    }
+}
diff --git a/src/com/android/ex/chips/RecipientAlternatesAdapter.java b/src/com/android/ex/chips/RecipientAlternatesAdapter.java
index b9a7c80..547a76b 100644
--- a/src/com/android/ex/chips/RecipientAlternatesAdapter.java
+++ b/src/com/android/ex/chips/RecipientAlternatesAdapter.java
@@ -27,15 +27,13 @@
 import android.text.util.Rfc822Token;
 import android.text.util.Rfc822Tokenizer;
 import android.util.Log;
-import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
 import android.widget.CursorAdapter;
-import android.widget.ImageView;
-import android.widget.TextView;
 
 import com.android.ex.chips.BaseRecipientAdapter.DirectoryListQuery;
 import com.android.ex.chips.BaseRecipientAdapter.DirectorySearchParams;
+import com.android.ex.chips.DropdownChipLayouter.AdapterType;
 import com.android.ex.chips.Queries.Query;
 
 import java.util.ArrayList;
@@ -51,7 +49,6 @@
  */
 public class RecipientAlternatesAdapter extends CursorAdapter {
     static final int MAX_LOOKUPS = 50;
-    private final LayoutInflater mLayoutInflater;
 
     private final long mCurrentId;
 
@@ -64,6 +61,7 @@
     public static final int QUERY_TYPE_EMAIL = 0;
     public static final int QUERY_TYPE_PHONE = 1;
     private Query mQuery;
+    private DropdownChipLayouter mDropdownChipLayouter;
 
     public interface RecipientMatchCallback {
         public void matchesFound(Map<String, RecipientEntry> results);
@@ -328,14 +326,9 @@
     }
 
     public RecipientAlternatesAdapter(Context context, long contactId, long currentId,
-            OnCheckedItemChangedListener listener) {
-        this(context, contactId, currentId, QUERY_TYPE_EMAIL, listener);
-    }
-
-    public RecipientAlternatesAdapter(Context context, long contactId, long currentId,
-            int queryMode, OnCheckedItemChangedListener listener) {
+            int queryMode, OnCheckedItemChangedListener listener,
+        DropdownChipLayouter dropdownChipLayouter) {
         super(context, getCursorForConstruction(context, contactId, queryMode), 0);
-        mLayoutInflater = LayoutInflater.from(context);
         mCurrentId = currentId;
         mCheckedItemChangedListener = listener;
 
@@ -347,6 +340,8 @@
             mQuery = Queries.EMAIL;
             Log.e(TAG, "Unsupported query type: " + queryMode);
         }
+
+        mDropdownChipLayouter = dropdownChipLayouter;
     }
 
     private static Cursor getCursorForConstruction(Context context, long contactId, int queryType) {
@@ -439,7 +434,7 @@
         Cursor cursor = getCursor();
         cursor.moveToPosition(position);
         if (convertView == null) {
-            convertView = newView();
+            convertView = mDropdownChipLayouter.newView();
         }
         if (cursor.getLong(Queries.Query.DATA_ID) == mCurrentId) {
             mCheckedItemPosition = position;
@@ -451,44 +446,18 @@
         return convertView;
     }
 
-    // TODO: this is VERY similar to the BaseRecipientAdapter. Can we combine
-    // somehow?
     @Override
     public void bindView(View view, Context context, Cursor cursor) {
         int position = cursor.getPosition();
-
-        TextView display = (TextView) view.findViewById(android.R.id.title);
-        ImageView imageView = (ImageView) view.findViewById(android.R.id.icon);
         RecipientEntry entry = getRecipientEntry(position);
-        if (position == 0) {
-            display.setText(cursor.getString(Queries.Query.NAME));
-            display.setVisibility(View.VISIBLE);
-            // TODO: see if this needs to be done outside the main thread
-            // as it may be too slow to get immediately.
-            imageView.setImageURI(entry.getPhotoThumbnailUri());
-            imageView.setVisibility(View.VISIBLE);
-        } else {
-            display.setVisibility(View.GONE);
-            imageView.setVisibility(View.GONE);
-        }
-        TextView destination = (TextView) view.findViewById(android.R.id.text1);
-        destination.setText(cursor.getString(Queries.Query.DESTINATION));
 
-        TextView destinationType = (TextView) view.findViewById(android.R.id.text2);
-        if (destinationType != null) {
-            destinationType.setText(mQuery.getTypeLabel(context.getResources(),
-                    cursor.getInt(Queries.Query.DESTINATION_TYPE),
-                    cursor.getString(Queries.Query.DESTINATION_LABEL)).toString().toUpperCase());
-        }
+        mDropdownChipLayouter.bindView(view, null, entry, position,
+                AdapterType.RECIPIENT_ALTERNATES, null);
     }
 
     @Override
     public View newView(Context context, Cursor cursor, ViewGroup parent) {
-        return newView();
-    }
-
-    private View newView() {
-        return mLayoutInflater.inflate(R.layout.chips_recipient_dropdown_item, null);
+        return mDropdownChipLayouter.newView();
     }
 
     /*package*/ static interface OnCheckedItemChangedListener {
diff --git a/src/com/android/ex/chips/RecipientEditTextView.java b/src/com/android/ex/chips/RecipientEditTextView.java
index 3cba87b..b355c1b 100644
--- a/src/com/android/ex/chips/RecipientEditTextView.java
+++ b/src/com/android/ex/chips/RecipientEditTextView.java
@@ -30,6 +30,7 @@
 import android.graphics.BitmapFactory;
 import android.graphics.Canvas;
 import android.graphics.Matrix;
+import android.graphics.Paint;
 import android.graphics.Point;
 import android.graphics.Rect;
 import android.graphics.RectF;
@@ -149,14 +150,24 @@
 
     private int mChipPadding;
 
+    /**
+     * Enumerator for avatar position. See attr.xml for more details.
+     * 0 for right, 1 for left.
+     */
+    private int mAvatarPosition;
+
+    private static final int AVATAR_POSITION_RIGHT = 0;
+
+    private static final int AVATAR_POSITION_LEFT = 1;
+
+    private boolean mDisableDelete;
+
     private Tokenizer mTokenizer;
 
     private Validator mValidator;
 
     private DrawableRecipientChip mSelectedChip;
 
-    private int mAlternatesLayout;
-
     private Bitmap mDefaultContactPhoto;
 
     private ImageSpan mMoreChip;
@@ -254,6 +265,8 @@
 
     private boolean mAttachedToWindow;
 
+    private DropdownChipLayouter mDropdownChipLayouter;
+
     public RecipientEditTextView(Context context, AttributeSet attrs) {
         super(context, attrs);
         setChipDimensions(context, attrs);
@@ -293,6 +306,12 @@
         addTextChangedListener(mTextWatcher);
         mGestureDetector = new GestureDetector(context, this);
         setOnEditorActionListener(this);
+
+        setDropdownChipLayouter(new DropdownChipLayouter(LayoutInflater.from(context), context));
+    }
+
+    protected void setDropdownChipLayouter(DropdownChipLayouter dropdownChipLayouter) {
+        mDropdownChipLayouter = dropdownChipLayouter;
     }
 
     @Override
@@ -433,17 +452,18 @@
     @Override
     public <T extends ListAdapter & Filterable> void setAdapter(T adapter) {
         super.setAdapter(adapter);
-        ((BaseRecipientAdapter) adapter)
-                .registerUpdateObserver(new BaseRecipientAdapter.EntriesUpdatedObserver() {
-                    @Override
-                    public void onChanged(List<RecipientEntry> entries) {
-                        // Scroll the chips field to the top of the screen so
-                        // that the user can see as many results as possible.
-                        if (entries != null && entries.size() > 0) {
-                            scrollBottomIntoView();
-                        }
-                    }
-                });
+        BaseRecipientAdapter baseAdapter = (BaseRecipientAdapter) adapter;
+        baseAdapter.registerUpdateObserver(new BaseRecipientAdapter.EntriesUpdatedObserver() {
+            @Override
+            public void onChanged(List<RecipientEntry> entries) {
+                // Scroll the chips field to the top of the screen so
+                // that the user can see as many results as possible.
+                if (entries != null && entries.size() > 0) {
+                    scrollBottomIntoView();
+                }
+            }
+        });
+        baseAdapter.setDropdownChipLayouter(mDropdownChipLayouter);
     }
 
     private void scrollBottomIntoView() {
@@ -543,124 +563,129 @@
                 TextUtils.TruncateAt.END);
     }
 
+    /**
+     * Creates a bitmap of the given contact on a selected chip.
+     *
+     * @param contact The recipient entry to pull data from.
+     * @param paint The paint to use to draw the bitmap.
+     */
     private Bitmap createSelectedChip(RecipientEntry contact, TextPaint paint) {
-        // Ellipsize the text so that it takes AT MOST the entire width of the
-        // autocomplete text entry area. Make sure to leave space for padding
-        // on the sides.
-        int height = (int) mChipHeight;
-        int deleteWidth = height;
-        float[] widths = new float[1];
-        paint.getTextWidths(" ", widths);
-        CharSequence ellipsizedText = ellipsizeText(createChipDisplayText(contact), paint,
-                calculateAvailableWidth() - deleteWidth - widths[0]);
-
-        // Make sure there is a minimum chip width so the user can ALWAYS
-        // tap a chip without difficulty.
-        int width = Math.max(deleteWidth * 2, (int) Math.floor(paint.measureText(ellipsizedText, 0,
-                ellipsizedText.length()))
-                + (mChipPadding * 2) + deleteWidth);
-
-        // Create the background of the chip.
-        Bitmap tmpBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
-        Canvas canvas = new Canvas(tmpBitmap);
-        if (mChipBackgroundPressed != null) {
-            mChipBackgroundPressed.setBounds(0, 0, width, height);
-            mChipBackgroundPressed.draw(canvas);
-            paint.setColor(sSelectedTextColor);
-            // Vertically center the text in the chip.
-            canvas.drawText(ellipsizedText, 0, ellipsizedText.length(), mChipPadding,
-                    getTextYOffset((String) ellipsizedText, paint, height), paint);
-            // Make the delete a square.
-            Rect backgroundPadding = new Rect();
-            mChipBackgroundPressed.getPadding(backgroundPadding);
-            mChipDelete.setBounds(width - deleteWidth + backgroundPadding.left,
-                    0 + backgroundPadding.top,
-                    width - backgroundPadding.right,
-                    height - backgroundPadding.bottom);
-            mChipDelete.draw(canvas);
+        paint.setColor(sSelectedTextColor);
+        Bitmap photo;
+        if (mDisableDelete) {
+            // Show the avatar instead if we don't want to delete
+            photo = getAvatarIcon(contact);
         } else {
-            Log.w(TAG, "Unable to draw a background for the chips as it was never set");
+            photo = ((BitmapDrawable) mChipDelete).getBitmap();
         }
-        return tmpBitmap;
+        return createChipBitmap(contact, paint, photo, mChipBackgroundPressed);
     }
 
-
+    /**
+     * Creates a bitmap of the given contact on a selected chip.
+     *
+     * @param contact The recipient entry to pull data from.
+     * @param paint The paint to use to draw the bitmap.
+     */
+    // TODO: Is leaveBlankIconSpacer obsolete now that we have left and right attributes?
     private Bitmap createUnselectedChip(RecipientEntry contact, TextPaint paint,
             boolean leaveBlankIconSpacer) {
+        Drawable background = getChipBackground(contact);
+        Bitmap photo = getAvatarIcon(contact);
+        paint.setColor(getContext().getResources().getColor(android.R.color.black));
+        return createChipBitmap(contact, paint, photo, background);
+    }
+
+    private Bitmap createChipBitmap(RecipientEntry contact, TextPaint paint, Bitmap icon,
+        Drawable background) {
+        if (background == null) {
+            Log.w(TAG, "Unable to draw a background for the chips as it was never set");
+            Bitmap.createBitmap((int) mChipHeight * 2, (int) mChipHeight, Bitmap.Config.ARGB_8888);
+        }
+
+        Rect backgroundPadding = new Rect();
+        background.getPadding(backgroundPadding);
+
         // Ellipsize the text so that it takes AT MOST the entire width of the
         // autocomplete text entry area. Make sure to leave space for padding
         // on the sides.
         int height = (int) mChipHeight;
-        int iconWidth = height;
+        // Since the icon is a square, it's width is equal to the maximum height it can be inside
+        // the chip.
+        int iconWidth = height - backgroundPadding.top - backgroundPadding.bottom;
         float[] widths = new float[1];
         paint.getTextWidths(" ", widths);
         CharSequence ellipsizedText = ellipsizeText(createChipDisplayText(contact), paint,
                 calculateAvailableWidth() - iconWidth - widths[0]);
+        int textWidth = (int) paint.measureText(ellipsizedText, 0, ellipsizedText.length());
+
         // Make sure there is a minimum chip width so the user can ALWAYS
         // tap a chip without difficulty.
-        int width = Math.max(iconWidth * 2, (int) Math.floor(paint.measureText(ellipsizedText, 0,
-                ellipsizedText.length()))
-                + (mChipPadding * 2) + iconWidth);
+        int width = Math.max(iconWidth * 2, textWidth + (mChipPadding * 2) + iconWidth
+                + backgroundPadding.left + backgroundPadding.right);
 
         // Create the background of the chip.
         Bitmap tmpBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
         Canvas canvas = new Canvas(tmpBitmap);
-        Drawable background = getChipBackground(contact);
-        if (background != null) {
-            background.setBounds(0, 0, width, height);
-            background.draw(canvas);
 
-            // Don't draw photos for recipients that have been typed in OR generated on the fly.
-            long contactId = contact.getContactId();
-            boolean drawPhotos = isPhoneQuery() ?
-                    contactId != RecipientEntry.INVALID_CONTACT
-                    : (contactId != RecipientEntry.INVALID_CONTACT
-                            && (contactId != RecipientEntry.GENERATED_CONTACT &&
-                                    !TextUtils.isEmpty(contact.getDisplayName())));
-            if (drawPhotos) {
-                byte[] photoBytes = contact.getPhotoBytes();
-                // There may not be a photo yet if anything but the first contact address
-                // was selected.
-                if (photoBytes == null && contact.getPhotoThumbnailUri() != null) {
-                    // TODO: cache this in the recipient entry?
-                    getAdapter().fetchPhoto(contact, contact.getPhotoThumbnailUri());
-                    photoBytes = contact.getPhotoBytes();
-                }
-
-                Bitmap photo;
-                if (photoBytes != null) {
-                    photo = BitmapFactory.decodeByteArray(photoBytes, 0, photoBytes.length);
-                } else {
-                    // TODO: can the scaled down default photo be cached?
-                    photo = mDefaultContactPhoto;
-                }
-                // Draw the photo on the left side.
-                if (photo != null) {
-                    RectF src = new RectF(0, 0, photo.getWidth(), photo.getHeight());
-                    Rect backgroundPadding = new Rect();
-                    mChipBackground.getPadding(backgroundPadding);
-                    RectF dst = new RectF(width - iconWidth + backgroundPadding.left,
-                            0 + backgroundPadding.top,
-                            width - backgroundPadding.right,
-                            height - backgroundPadding.bottom);
-                    Matrix matrix = new Matrix();
-                    matrix.setRectToRect(src, dst, Matrix.ScaleToFit.FILL);
-                    canvas.drawBitmap(photo, matrix, paint);
-                }
-            } else if (!leaveBlankIconSpacer || isPhoneQuery()) {
-                iconWidth = 0;
-            }
-            paint.setColor(getContext().getResources().getColor(android.R.color.black));
-            // Vertically center the text in the chip.
-            canvas.drawText(ellipsizedText, 0, ellipsizedText.length(), mChipPadding,
-                    getTextYOffset((String)ellipsizedText, paint, height), paint);
-        } else {
-            Log.w(TAG, "Unable to draw a background for the chips as it was never set");
+        // Draw the background drawable
+        background.setBounds(0, 0, width, height);
+        background.draw(canvas);
+        // Draw the text vertically aligned
+        int textX = mAvatarPosition == AVATAR_POSITION_RIGHT ?
+                mChipPadding + backgroundPadding.left :
+                width - backgroundPadding.right - mChipPadding - textWidth;
+        canvas.drawText(ellipsizedText, 0, ellipsizedText.length(),
+                textX, getTextYOffset(ellipsizedText.toString(), paint, height), paint);
+        if (icon != null) {
+            // Draw the icon
+            int iconX = mAvatarPosition == AVATAR_POSITION_RIGHT ?
+                    width - backgroundPadding.right - iconWidth :
+                    backgroundPadding.left;
+            RectF src = new RectF(0, 0, icon.getWidth(), icon.getHeight());
+            RectF dst = new RectF(iconX,
+                    0 + backgroundPadding.top,
+                    iconX + iconWidth,
+                    height - backgroundPadding.bottom);
+            drawIconOnCanvas(icon, canvas, paint, src, dst);
         }
         return tmpBitmap;
     }
 
     /**
+     * Returns the avatar icon to use for this recipient entry. Returns null if we don't want to
+     * draw an icon for this recipient.
+     */
+    private Bitmap getAvatarIcon(RecipientEntry contact) {
+        // Don't draw photos for recipients that have been typed in OR generated on the fly.
+        long contactId = contact.getContactId();
+        boolean drawPhotos = isPhoneQuery() ?
+                contactId != RecipientEntry.INVALID_CONTACT
+                : (contactId != RecipientEntry.INVALID_CONTACT
+                        && (contactId != RecipientEntry.GENERATED_CONTACT &&
+                                !TextUtils.isEmpty(contact.getDisplayName())));
+
+        if (drawPhotos) {
+            byte[] photoBytes = contact.getPhotoBytes();
+            // There may not be a photo yet if anything but the first contact address
+            // was selected.
+            if (photoBytes == null && contact.getPhotoThumbnailUri() != null) {
+                // TODO: cache this in the recipient entry?
+                getAdapter().fetchPhoto(contact, contact.getPhotoThumbnailUri());
+                photoBytes = contact.getPhotoBytes();
+            }
+            if (photoBytes != null) {
+                return BitmapFactory.decodeByteArray(photoBytes, 0, photoBytes.length);
+            } else {
+                // TODO: can the scaled down default photo be cached?
+                return mDefaultContactPhoto;
+            }
+        }
+
+        return null;
+    }
+
+    /**
      * Get the background drawable for a RecipientChip.
      */
     // Visible for testing.
@@ -668,13 +693,26 @@
         return contact.isValid() ? mChipBackground : mInvalidChipBackground;
     }
 
-    private static float getTextYOffset(String text, TextPaint paint, int height) {
+    /**
+     * Given a height, returns a Y offset that will draw the text in the middle of the height.
+     */
+    protected float getTextYOffset(String text, TextPaint paint, int height) {
         Rect bounds = new Rect();
         paint.getTextBounds(text, 0, text.length(), bounds);
         int textHeight = bounds.bottom - bounds.top ;
         return height - ((height - textHeight) / 2) - (int)paint.descent();
     }
 
+    /**
+     * Draws the icon onto the canvas given the source rectangle of the bitmap and the destination 
+     * rectangle of the canvas.
+     */
+    protected void drawIconOnCanvas(Bitmap icon, Canvas canvas, Paint paint, RectF src, RectF dst) {
+        Matrix matrix = new Matrix();
+        matrix.setRectToRect(src, dst, Matrix.ScaleToFit.FILL);
+        canvas.drawBitmap(icon, matrix, paint);
+    }
+
     private DrawableRecipientChip constructChipSpan(RecipientEntry contact, boolean pressed,
             boolean leaveIconSpace) throws NullPointerException {
         if (mChipBackground == null) {
@@ -749,11 +787,6 @@
         if (mChipPadding == -1) {
             mChipPadding = (int) r.getDimension(R.dimen.chip_padding);
         }
-        mAlternatesLayout = a.getResourceId(R.styleable.RecipientEditTextView_chipAlternatesLayout,
-                -1);
-        if (mAlternatesLayout == -1) {
-            mAlternatesLayout = R.layout.chips_alternate_item;
-        }
 
         mDefaultContactPhoto = BitmapFactory.decodeResource(r, R.drawable.ic_contact_picture);
 
@@ -772,6 +805,9 @@
         if (mInvalidChipBackground == null) {
             mInvalidChipBackground = r.getDrawable(R.drawable.chip_background_invalid);
         }
+        mAvatarPosition = a.getInt(R.styleable.RecipientEditTextView_avatarPosition, 0);
+        mDisableDelete = a.getBoolean(R.styleable.RecipientEditTextView_avatarPosition, false);
+
         mLineSpacingExtra =  r.getDimension(R.dimen.line_spacing_extra);
         mMaxLines = r.getInteger(R.integer.chips_max_lines);
         TypedValue tv = new TypedValue();
@@ -779,6 +815,7 @@
             mActionBarHeight = TypedValue.complexToDimensionPixelSize(tv.data, getResources()
                     .getDisplayMetrics());
         }
+
         a.recycle();
     }
 
@@ -798,6 +835,10 @@
         mChipHeight = height;
     }
 
+    public float getChipHeight() {
+        return mChipHeight;
+    }
+
     /**
      * Set whether to shrink the recipients field such that at most
      * one line of recipients chips are shown when the field loses
@@ -1511,12 +1552,12 @@
 
     private ListAdapter createAlternatesAdapter(DrawableRecipientChip chip) {
         return new RecipientAlternatesAdapter(getContext(), chip.getContactId(), chip.getDataId(),
-                getAdapter().getQueryType(), this);
+                getAdapter().getQueryType(), this, mDropdownChipLayouter);
     }
 
     private ListAdapter createSingleAddressAdapter(DrawableRecipientChip currentChip) {
-        return new SingleRecipientArrayAdapter(getContext(), mAlternatesLayout, currentChip
-                .getEntry());
+        return new SingleRecipientArrayAdapter(getContext(), currentChip.getEntry(),
+                mDropdownChipLayouter);
     }
 
     @Override
@@ -2121,14 +2162,20 @@
         // Figure out the bounds of this chip and whether or not
         // the user clicked in the X portion.
         // TODO: Should x and y be used, or removed?
-        return chip.isSelected() && offset == getChipEnd(chip);
+        if (mDisableDelete) {
+            return false;
+        }
+
+        return chip.isSelected() &&
+                ((mAvatarPosition == 0 && offset == getChipEnd(chip)) ||
+                (mAvatarPosition != 0 && offset == getChipStart(chip)));
     }
 
     /**
      * Remove the chip and any text associated with it from the RecipientEditTextView.
      */
     // Visible for testing.
-    /*pacakge*/ void removeChip(DrawableRecipientChip chip) {
+    /* package */void removeChip(DrawableRecipientChip chip) {
         Spannable spannable = getSpannable();
         int spanStart = spannable.getSpanStart(chip);
         int spanEnd = spannable.getSpanEnd(chip);
diff --git a/src/com/android/ex/chips/SingleRecipientArrayAdapter.java b/src/com/android/ex/chips/SingleRecipientArrayAdapter.java
index 0571a4e..985953f 100644
--- a/src/com/android/ex/chips/SingleRecipientArrayAdapter.java
+++ b/src/com/android/ex/chips/SingleRecipientArrayAdapter.java
@@ -17,47 +17,27 @@
 package com.android.ex.chips;
 
 import android.content.Context;
-import android.text.util.Rfc822Tokenizer;
-import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
 import android.widget.ArrayAdapter;
-import android.widget.ImageView;
-import android.widget.TextView;
+
+import com.android.ex.chips.DropdownChipLayouter.AdapterType;
 
 class SingleRecipientArrayAdapter extends ArrayAdapter<RecipientEntry> {
-    private int mLayoutId;
+    private final DropdownChipLayouter mDropdownChipLayouter;
 
-    private final LayoutInflater mLayoutInflater;
-
-    public SingleRecipientArrayAdapter(Context context, int resourceId, RecipientEntry entry) {
-        super(context, resourceId, new RecipientEntry[] {
+    public SingleRecipientArrayAdapter(Context context, RecipientEntry entry,
+            DropdownChipLayouter dropdownChipLayouter) {
+        super(context, dropdownChipLayouter.getAlternateItemLayoutResId(), new RecipientEntry[] {
             entry
         });
-        mLayoutInflater = LayoutInflater.from(context);
-        mLayoutId = resourceId;
+
+        mDropdownChipLayouter = dropdownChipLayouter;
     }
 
     @Override
     public View getView(int position, View convertView, ViewGroup parent) {
-        if (convertView == null) {
-            convertView = newView();
-        }
-        bindView(convertView, getItem(position));
-        return convertView;
-    }
-
-    private View newView() {
-        return mLayoutInflater.inflate(mLayoutId, null);
-    }
-
-    private static void bindView(View view, RecipientEntry entry) {
-        TextView display = (TextView) view.findViewById(android.R.id.title);
-        ImageView imageView = (ImageView) view.findViewById(android.R.id.icon);
-        display.setText(entry.getDisplayName());
-        display.setVisibility(View.VISIBLE);
-        imageView.setVisibility(View.VISIBLE);
-        TextView destination = (TextView) view.findViewById(android.R.id.text1);
-        destination.setText(Rfc822Tokenizer.tokenize(entry.getDestination())[0].getAddress());
+        return mDropdownChipLayouter.bindView(convertView, parent, getItem(position), position,
+                AdapterType.SINGLE_RECIPIENT, null);
     }
 }