Add UI for the "invite"

- Add "More networks" button to the contact card networks section,
  which opens the list of invitable account types in a popup.
  (This "More networks" section reuses NetworkTitleViewEntry.)
- The list popup uses the same layout as the account selector
- Updated the account selector layout according to the rough visual
  spec.   Removed the tablet variant, as we use the same layout
  on the tablet too for now.
- Sends the invite intent when a type is selected.

Bug 5061956

Change-Id: I0a62126a0cf4ffeecc1a7263b124d00201b67e21
diff --git a/src/com/android/contacts/ContactLoader.java b/src/com/android/contacts/ContactLoader.java
index 4078598..3463d3c 100644
--- a/src/com/android/contacts/ContactLoader.java
+++ b/src/com/android/contacts/ContactLoader.java
@@ -730,6 +730,11 @@
             }
         }
 
+        /**
+         * Sets the "invitable" account types to {@link Result#mInvitableAccountTypes}.
+         *
+         * TODO Exclude the ones with no raw contacts in the database.
+         */
         private void loadInvitableAccountTypes(Result contactData) {
             Map<String, AccountType> allInvitables =
                     AccountTypeManager.getInstance(getContext()).getInvitableAccountTypes();
diff --git a/src/com/android/contacts/detail/ContactDetailFragment.java b/src/com/android/contacts/detail/ContactDetailFragment.java
index 17df4b4..a6ba6b0 100644
--- a/src/com/android/contacts/detail/ContactDetailFragment.java
+++ b/src/com/android/contacts/detail/ContactDetailFragment.java
@@ -100,6 +100,8 @@
 import android.widget.Button;
 import android.widget.CheckBox;
 import android.widget.ImageView;
+import android.widget.ListAdapter;
+import android.widget.ListPopupWindow;
 import android.widget.ListView;
 import android.widget.TextView;
 import android.widget.Toast;
@@ -752,7 +754,8 @@
         String attribution = ContactDetailDisplayUtils.getAttribution(mContext, mContactData);
         boolean hasAttribution = !TextUtils.isEmpty(attribution);
         int networksCount = mOtherEntriesMap.keySet().size();
-        if (!hasAttribution && networksCount == 0) {
+        int invitableCount = mContactData.getInvitableAccontTypes().size();
+        if (!hasAttribution && networksCount == 0 && invitableCount == 0) {
             return;
         }
 
@@ -777,7 +780,7 @@
         for (AccountType accountType : mOtherEntriesMap.keySet()) {
 
             // Add a title for each third party app
-            mAllEntries.add(new NetworkTitleViewEntry(accountType));
+            mAllEntries.add(NetworkTitleViewEntry.fromAccountType(mContext, accountType));
 
             for (DetailViewEntry detailEntry : mOtherEntriesMap.get(accountType)) {
                 // Add indented separator
@@ -792,6 +795,46 @@
         }
 
         mOtherEntriesMap.clear();
+
+        // Add the "More networks" button, which opens the invitable account type list popup.
+        if (invitableCount > 0) {
+            addMoreNetworks();
+        }
+    }
+
+    /**
+     * Add the "More networks" entry.  When clicked, show a popup containing a list of invitable
+     * account types.
+     */
+    private void addMoreNetworks() {
+        // First, prepare for the popup.
+
+        // Adapter for the list popup.
+        final InvitableAccountTypesAdapter popupAdapter = new InvitableAccountTypesAdapter(mContext,
+                mContactData);
+
+        // Listener called when a popup item is clicked.
+        final AdapterView.OnItemClickListener popupItemListener
+                = new AdapterView.OnItemClickListener() {
+            @Override
+            public void onItemClick(AdapterView<?> parent, View view, int position,
+                    long id) {
+                if (mListener != null) {
+                    mListener.onItemClicked(popupAdapter.getIntent(mContext, position));
+                }
+            }
+        };
+
+        // Then create the click listener for the "More network" entry.  Open the popup.
+        View.OnClickListener onClickListener = new OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                showListPopup(v, popupAdapter, popupItemListener);
+            }
+        };
+
+        // Finally create the entry.
+        mAllEntries.add(NetworkTitleViewEntry.forMoreNetworks(mContext, onClickListener));
     }
 
     /**
@@ -927,6 +970,30 @@
     }
 
     /**
+     * Show a list popup.  Used for "popup-able" entry, such as "More networks".
+     */
+    private void showListPopup(View anchorView, ListAdapter adapter,
+            final AdapterView.OnItemClickListener onItemClickListener) {
+        final ListPopupWindow popup = new ListPopupWindow(mContext, null);
+        popup.setAnchorView(anchorView);
+        popup.setWidth(anchorView.getWidth());
+        popup.setAdapter(adapter);
+        popup.setModal(true);
+
+        // We need to wrap the passed onItemClickListener here, so that we can dismiss() the
+        // popup afterwards.  Otherwise we could directly use the passed listener.
+        popup.setOnItemClickListener(new AdapterView.OnItemClickListener() {
+            @Override
+            public void onItemClick(AdapterView<?> parent, View view, int position,
+                    long id) {
+                onItemClickListener.onItemClick(parent, view, position, id);
+                popup.dismiss();
+            }
+        });
+        popup.show();
+    }
+
+    /**
      * Base class for an item in the {@link ViewAdapter} list of data, which is
      * supplied to the {@link ListView}.
      */
@@ -951,6 +1018,16 @@
         boolean isEnabled(){
             return isEnabled;
         }
+
+        /**
+         * Called when the entry is clicked.  Only {@link #isEnabled} entries can get clicked.
+         *
+         * @param clickedView  {@link View} that was clicked  (Used, for example, as the anchor view
+         *        for a popup.)
+         * @param fragmentListener  {@link Listener} set to {@link ContactDetailFragment}
+         */
+        public void click(View clickedView, Listener fragmentListener) {
+        }
     }
 
     /**
@@ -1008,19 +1085,49 @@
     }
 
     /**
-     * A title for a section of contact details from a single 3rd party network.
+     * A title for a section of contact details from a single 3rd party network.  It's also
+     * used for the "More networks" entry, which has the same layout.
      */
     private static class NetworkTitleViewEntry extends ViewEntry {
+        private final Drawable mIcon;
+        private final CharSequence mLabel;
+        private final View.OnClickListener mOnClickListener;
 
-        private final AccountType mAccountType;
-
-        NetworkTitleViewEntry(AccountType type) {
+        private NetworkTitleViewEntry(Drawable icon, CharSequence label, View.OnClickListener
+                onClickListener) {
             super(ViewAdapter.VIEW_TYPE_NETWORK_TITLE_ENTRY);
-            mAccountType = type;
+            this.mIcon = icon;
+            this.mLabel = label;
+            this.mOnClickListener = onClickListener;
+            this.isEnabled = onClickListener != null;
         }
 
-        public AccountType getAccountType() {
-            return mAccountType;
+        public static NetworkTitleViewEntry fromAccountType(Context context, AccountType type) {
+            return new NetworkTitleViewEntry(
+                    type.getDisplayIcon(context), type.getDisplayLabel(context), null);
+        }
+
+        public static NetworkTitleViewEntry forMoreNetworks(Context context, View.OnClickListener
+                onClickListener) {
+            // TODO Icon is temporary.  Need proper one.
+            return new NetworkTitleViewEntry(
+                    context.getResources().getDrawable(R.drawable.ic_menu_add_field_holo_light),
+                    context.getString(R.string.more_networks_button),
+                    onClickListener);
+        }
+
+        @Override
+        public void click(View clickedView, Listener fragmentListener) {
+            if (mOnClickListener == null) return;
+            mOnClickListener.onClick(clickedView);
+        }
+
+        public Drawable getIcon() {
+            return mIcon;
+        }
+
+        public CharSequence getLabel() {
+            return mLabel;
         }
     }
 
@@ -1187,6 +1294,12 @@
 
             return true;
         }
+
+        @Override
+        public void click(View clickedView, Listener fragmentListener) {
+            if (fragmentListener == null || intent == null) return;
+            fragmentListener.onItemClicked(intent);
+        }
     }
 
     /**
@@ -1348,8 +1461,8 @@
                 result.setTag(viewCache);
             }
 
-            viewCache.name.setText(entry.getAccountType().getDisplayLabel(mContext));
-            viewCache.icon.setImageDrawable(entry.getAccountType().getDisplayIcon(mContext));
+            viewCache.name.setText(entry.getLabel());
+            viewCache.icon.setImageDrawable(entry.getIcon());
 
             return result;
         }
@@ -1524,10 +1637,8 @@
     public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
         if (mListener == null) return;
         final ViewEntry entry = mAdapter.getItem(position);
-        if (entry == null || !(entry instanceof DetailViewEntry)) return;
-        final Intent intent = ((DetailViewEntry) entry).intent;
-        if (intent == null) return;
-        mListener.onItemClicked(intent);
+        if (entry == null) return;
+        entry.click(view, mListener);
     }
 
     @Override
@@ -1789,4 +1900,84 @@
          */
         public void onCreateRawContactRequested(ArrayList<ContentValues> values, Account account);
     }
+
+    /**
+     * Adapter for the invitable account types; used for the invitable account type list popup.
+     */
+    private final static class InvitableAccountTypesAdapter extends BaseAdapter {
+        private final Context mContext;
+        private final LayoutInflater mInflater;
+        private final ContactLoader.Result mContactData;
+        private final ArrayList<AccountType> mAccountTypes;
+
+        public InvitableAccountTypesAdapter(Context context, ContactLoader.Result contactData) {
+            mContext = context;
+            mInflater = LayoutInflater.from(context);
+            mContactData = contactData;
+            final List<String> types = contactData.getInvitableAccontTypes();
+            mAccountTypes = new ArrayList<AccountType>(types.size());
+
+            AccountTypeManager manager = AccountTypeManager.getInstance(context);
+            for (int i = 0; i < types.size(); i++) {
+                mAccountTypes.add(manager.getAccountType(types.get(i)));
+            }
+
+            Collections.sort(mAccountTypes, new AccountType.DisplayLabelComparator(mContext));
+        }
+
+        @Override
+        public View getView(int position, View convertView, ViewGroup parent) {
+            final View resultView =
+                    (convertView != null) ? convertView
+                    : mInflater.inflate(R.layout.account_selector_list_item, parent, false);
+
+            final TextView text1 = (TextView)resultView.findViewById(android.R.id.text1);
+            final TextView text2 = (TextView)resultView.findViewById(android.R.id.text2);
+            final ImageView icon = (ImageView)resultView.findViewById(android.R.id.icon);
+
+            final AccountType accountType = mAccountTypes.get(position);
+
+            CharSequence action = accountType.getInviteContactActionLabel(mContext);
+            CharSequence label = accountType.getDisplayLabel(mContext);
+            if (TextUtils.isEmpty(action)) {
+                text1.setText(label);
+                text2.setVisibility(View.GONE);
+            } else {
+                text1.setText(action);
+                text2.setVisibility(View.VISIBLE);
+                text2.setText(label);
+            }
+            icon.setImageDrawable(accountType.getDisplayIcon(mContext));
+
+            return resultView;
+        }
+
+        public Intent getIntent(Context context, int position) {
+            final AccountType accountType = mAccountTypes.get(position);
+            Intent intent = new Intent();
+            intent.setClassName(accountType.resPackageName,
+                    accountType.getInviteContactActivityClassName());
+
+            intent.setAction(ContactsContract.Intents.INVITE_CONTACT);
+
+            // Data is the lookup URI.
+            intent.setData(mContactData.getLookupUri());
+            return intent;
+        }
+
+        @Override
+        public int getCount() {
+            return mAccountTypes.size();
+        }
+
+        @Override
+        public AccountType getItem(int position) {
+            return mAccountTypes.get(position);
+        }
+
+        @Override
+        public long getItemId(int position) {
+            return position;
+        }
+    }
 }
diff --git a/src/com/android/contacts/detail/ContactLoaderFragment.java b/src/com/android/contacts/detail/ContactLoaderFragment.java
index daa6012..9085670 100644
--- a/src/com/android/contacts/detail/ContactLoaderFragment.java
+++ b/src/com/android/contacts/detail/ContactLoaderFragment.java
@@ -171,7 +171,7 @@
         public Loader<ContactLoader.Result> onCreateLoader(int id, Bundle args) {
             Uri lookupUri = args.getParcelable(LOADER_ARG_CONTACT_URI);
             return new ContactLoader(mContext, lookupUri, true /* loadGroupMetaData */,
-                    true /* loadStreamItems */, false /* load invitable account types */);
+                    true /* loadStreamItems */, true /* load invitable account types */);
         }
 
         @Override
diff --git a/src/com/android/contacts/util/AccountsListAdapter.java b/src/com/android/contacts/util/AccountsListAdapter.java
index 1a8b3ea..5448d1d 100644
--- a/src/com/android/contacts/util/AccountsListAdapter.java
+++ b/src/com/android/contacts/util/AccountsListAdapter.java
@@ -22,6 +22,7 @@
 
 import android.accounts.Account;
 import android.content.Context;
+import android.text.TextUtils.TruncateAt;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
@@ -77,9 +78,11 @@
         final AccountType accountType = mAccountTypes.getAccountType(account.type);
 
         text1.setText(account.name);
-        if (text2 != null) {
-            text2.setText(accountType.getDisplayLabel(mContext));
-        }
+
+        // For email addresses, we don't want to truncate at end, which might cut off the domain
+        // name.
+        text1.setEllipsize(TruncateAt.MIDDLE);
+        text2.setText(accountType.getDisplayLabel(mContext));
         icon.setImageDrawable(accountType.getDisplayIcon(mContext));
 
         return resultView;