summaryrefslogtreecommitdiffstats
path: root/src/com/android/messaging/ui/contact
diff options
context:
space:
mode:
Diffstat (limited to 'src/com/android/messaging/ui/contact')
-rw-r--r--src/com/android/messaging/ui/contact/AddContactsConfirmationDialog.java85
-rw-r--r--src/com/android/messaging/ui/contact/AllContactsListViewHolder.java62
-rw-r--r--src/com/android/messaging/ui/contact/ContactDropdownLayouter.java138
-rw-r--r--src/com/android/messaging/ui/contact/ContactListAdapter.java86
-rw-r--r--src/com/android/messaging/ui/contact/ContactListItemView.java177
-rw-r--r--src/com/android/messaging/ui/contact/ContactPickerFragment.java607
-rw-r--r--src/com/android/messaging/ui/contact/ContactRecipientAdapter.java286
-rw-r--r--src/com/android/messaging/ui/contact/ContactRecipientAutoCompleteView.java289
-rw-r--r--src/com/android/messaging/ui/contact/ContactRecipientPhotoManager.java96
-rw-r--r--src/com/android/messaging/ui/contact/ContactSectionIndexer.java169
-rw-r--r--src/com/android/messaging/ui/contact/FrequentContactsListViewHolder.java63
11 files changed, 2058 insertions, 0 deletions
diff --git a/src/com/android/messaging/ui/contact/AddContactsConfirmationDialog.java b/src/com/android/messaging/ui/contact/AddContactsConfirmationDialog.java
new file mode 100644
index 0000000..9c1393d
--- /dev/null
+++ b/src/com/android/messaging/ui/contact/AddContactsConfirmationDialog.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.messaging.ui.contact;
+
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.res.Resources;
+import android.net.Uri;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.Button;
+import android.widget.TextView;
+
+import com.android.messaging.R;
+import com.android.messaging.ui.ContactIconView;
+import com.android.messaging.ui.UIIntents;
+import com.android.messaging.util.AccessibilityUtil;
+
+public class AddContactsConfirmationDialog implements DialogInterface.OnClickListener {
+ private final Context mContext;
+ private final Uri mAvatarUri;
+ private final String mNormalizedDestination;
+
+ public AddContactsConfirmationDialog(final Context context, final Uri avatarUri,
+ final String normalizedDestination) {
+ mContext = context;
+ mAvatarUri = avatarUri;
+ mNormalizedDestination = normalizedDestination;
+ }
+
+ public void show() {
+ final int confirmAddContactStringId = R.string.add_contact_confirmation;
+ final int cancelStringId = android.R.string.cancel;
+ final AlertDialog alertDialog = new AlertDialog.Builder(mContext)
+ .setTitle(R.string.add_contact_confirmation_dialog_title)
+ .setView(createBodyView())
+ .setPositiveButton(confirmAddContactStringId, this)
+ .setNegativeButton(cancelStringId, null)
+ .create();
+ alertDialog.show();
+ final Resources resources = mContext.getResources();
+ final Button cancelButton = alertDialog.getButton(DialogInterface.BUTTON_NEGATIVE);
+ if (cancelButton != null) {
+ cancelButton.setTextColor(resources.getColor(R.color.contact_picker_button_text_color));
+ }
+ final Button addButton = alertDialog.getButton(DialogInterface.BUTTON_POSITIVE);
+ if (addButton != null) {
+ addButton.setTextColor(resources.getColor(R.color.contact_picker_button_text_color));
+ }
+ }
+
+ @Override
+ public void onClick(final DialogInterface dialog, final int which) {
+ UIIntents.get().launchAddContactActivity(mContext, mNormalizedDestination);
+ }
+
+ private View createBodyView() {
+ final View view = LayoutInflater.from(mContext).inflate(
+ R.layout.add_contacts_confirmation_dialog_body, null);
+ final ContactIconView iconView = (ContactIconView) view.findViewById(R.id.contact_icon);
+ iconView.setImageResourceUri(mAvatarUri);
+ final TextView textView = (TextView) view.findViewById(R.id.participant_name);
+ textView.setText(mNormalizedDestination);
+ // Accessibility reason : in case phone numbers are mixed in the display name,
+ // we need to vocalize it for talkback.
+ final String vocalizedDisplayName = AccessibilityUtil.getVocalizedPhoneNumber(
+ mContext.getResources(), mNormalizedDestination);
+ textView.setContentDescription(vocalizedDisplayName);
+ return view;
+ }
+}
diff --git a/src/com/android/messaging/ui/contact/AllContactsListViewHolder.java b/src/com/android/messaging/ui/contact/AllContactsListViewHolder.java
new file mode 100644
index 0000000..7263c54
--- /dev/null
+++ b/src/com/android/messaging/ui/contact/AllContactsListViewHolder.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.messaging.ui.contact;
+
+import android.content.Context;
+
+import com.android.messaging.R;
+import com.android.messaging.ui.CustomHeaderPagerListViewHolder;
+import com.android.messaging.ui.contact.ContactListItemView.HostInterface;
+
+/**
+ * Holds the all contacts view for the contact picker's view pager.
+ */
+public class AllContactsListViewHolder extends CustomHeaderPagerListViewHolder {
+ public AllContactsListViewHolder(final Context context, final HostInterface clivHostInterface) {
+ super(context, new ContactListAdapter(context, null, clivHostInterface,
+ true /* needAlphabetHeader */));
+ }
+
+ @Override
+ protected int getLayoutResId() {
+ return R.layout.all_contacts_list_view;
+ }
+
+ @Override
+ protected int getPageTitleResId() {
+ return R.string.contact_picker_all_contacts_tab_title;
+ }
+
+ @Override
+ protected int getEmptyViewResId() {
+ return R.id.empty_view;
+ }
+
+ @Override
+ protected int getListViewResId() {
+ return R.id.all_contacts_list;
+ }
+
+ @Override
+ protected int getEmptyViewTitleResId() {
+ return R.string.contact_list_empty_text;
+ }
+
+ @Override
+ protected int getEmptyViewImageResId() {
+ return R.drawable.ic_oobe_freq_list;
+ }
+}
diff --git a/src/com/android/messaging/ui/contact/ContactDropdownLayouter.java b/src/com/android/messaging/ui/contact/ContactDropdownLayouter.java
new file mode 100644
index 0000000..7df62de
--- /dev/null
+++ b/src/com/android/messaging/ui/contact/ContactDropdownLayouter.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.messaging.ui.contact;
+
+import android.content.Context;
+import android.graphics.drawable.StateListDrawable;
+import android.net.Uri;
+import android.support.v4.text.BidiFormatter;
+import android.support.v4.text.TextDirectionHeuristicsCompat;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+
+import com.android.ex.chips.DropdownChipLayouter;
+import com.android.ex.chips.RecipientEntry;
+import com.android.messaging.R;
+import com.android.messaging.datamodel.data.ContactListItemData;
+import com.android.messaging.datamodel.data.ParticipantData;
+import com.android.messaging.ui.ContactIconView;
+import com.android.messaging.util.Assert;
+import com.android.messaging.util.AvatarUriUtil;
+import com.android.messaging.util.ContactRecipientEntryUtils;
+
+/**
+ * An implementation for {@link DropdownChipLayouter}. Layouts the dropdown
+ * list in the ContactRecipientAutoCompleteView in Material style.
+ */
+public class ContactDropdownLayouter extends DropdownChipLayouter {
+ private final ContactListItemView.HostInterface mClivHostInterface;
+
+ public ContactDropdownLayouter(final LayoutInflater inflater, final Context context,
+ final ContactListItemView.HostInterface clivHostInterface) {
+ super(inflater, context);
+ mClivHostInterface = new ContactListItemView.HostInterface() {
+
+ @Override
+ public void onContactListItemClicked(final ContactListItemData item,
+ final ContactListItemView view) {
+ // The chips UI will handle auto-complete item click events, so No-op here.
+ }
+
+ @Override
+ public boolean isContactSelected(final ContactListItemData item) {
+ // In chips drop down we don't show any selected checkmark per design.
+ return false;
+ }
+ };
+ }
+
+ /**
+ * Bind a drop down view to a RecipientEntry. We'd like regular dropdown items (BASE_RECIPIENT)
+ * to behave the same as regular ContactListItemViews, while using the chips library's
+ * item styling for alternates dropdown items (happens when you click on a chip).
+ */
+ @Override
+ public View bindView(final View convertView, final ViewGroup parent, final RecipientEntry entry,
+ final int position, AdapterType type, final String substring,
+ final StateListDrawable deleteDrawable) {
+ if (type != AdapterType.BASE_RECIPIENT) {
+ if (type == AdapterType.SINGLE_RECIPIENT) {
+ // Treat single recipients the same way as alternates. The base implementation of
+ // single recipients would try to simplify the destination by tokenizing. We'd
+ // like to always show the full destination address per design request.
+ type = AdapterType.RECIPIENT_ALTERNATES;
+ }
+ return super.bindView(convertView, parent, entry, position, type, substring,
+ deleteDrawable);
+ }
+
+ // Default to show all the information
+ // RTL : To format contact name and detail if they happen to be phone numbers.
+ final BidiFormatter bidiFormatter = BidiFormatter.getInstance();
+ final String displayName = bidiFormatter.unicodeWrap(
+ ContactRecipientEntryUtils.getDisplayNameForContactList(entry),
+ TextDirectionHeuristicsCompat.LTR);
+ final String destination = bidiFormatter.unicodeWrap(
+ ContactRecipientEntryUtils.formatDestination(entry),
+ TextDirectionHeuristicsCompat.LTR);
+ final View itemView = reuseOrInflateView(convertView, parent, type);
+
+ // Bold the string that is matched.
+ final CharSequence[] styledResults =
+ getStyledResults(substring, displayName, destination);
+
+ Assert.isTrue(itemView instanceof ContactListItemView);
+ final ContactListItemView contactListItemView = (ContactListItemView) itemView;
+ contactListItemView.setImageClickHandlerDisabled(true);
+ contactListItemView.bind(entry, styledResults[0], styledResults[1],
+ mClivHostInterface, (type == AdapterType.SINGLE_RECIPIENT));
+ return itemView;
+ }
+
+ @Override
+ protected void bindIconToView(boolean showImage, RecipientEntry entry, ImageView view,
+ AdapterType type) {
+ if (showImage && view instanceof ContactIconView) {
+ final ContactIconView contactView = (ContactIconView) view;
+ // These show contact cards by default, but that isn't what we want here
+ contactView.setImageClickHandlerDisabled(true);
+ final Uri avatarUri = AvatarUriUtil.createAvatarUri(
+ ParticipantData.getFromRecipientEntry(entry));
+ contactView.setImageResourceUri(avatarUri);
+ } else {
+ super.bindIconToView(showImage, entry, view, type);
+ }
+ }
+
+ @Override
+ protected int getItemLayoutResId(AdapterType type) {
+ switch (type) {
+ case BASE_RECIPIENT:
+ return R.layout.contact_list_item_view;
+ case RECIPIENT_ALTERNATES:
+ return R.layout.chips_alternates_dropdown_item;
+ default:
+ return R.layout.chips_alternates_dropdown_item;
+ }
+ }
+
+ @Override
+ protected int getAlternateItemLayoutResId(AdapterType type) {
+ return getItemLayoutResId(type);
+ }
+}
diff --git a/src/com/android/messaging/ui/contact/ContactListAdapter.java b/src/com/android/messaging/ui/contact/ContactListAdapter.java
new file mode 100644
index 0000000..d466b61
--- /dev/null
+++ b/src/com/android/messaging/ui/contact/ContactListAdapter.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.messaging.ui.contact;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.CursorAdapter;
+import android.widget.SectionIndexer;
+
+import com.android.messaging.R;
+import com.android.messaging.util.Assert;
+
+public class ContactListAdapter extends CursorAdapter implements SectionIndexer {
+ private final ContactListItemView.HostInterface mClivHostInterface;
+ private final boolean mNeedAlphabetHeader;
+ private ContactSectionIndexer mSectionIndexer;
+
+ public ContactListAdapter(final Context context, final Cursor cursor,
+ final ContactListItemView.HostInterface clivHostInterface,
+ final boolean needAlphabetHeader) {
+ super(context, cursor, 0);
+ mClivHostInterface = clivHostInterface;
+ mNeedAlphabetHeader = needAlphabetHeader;
+ mSectionIndexer = new ContactSectionIndexer(cursor);
+ }
+
+ @Override
+ public void bindView(final View view, final Context context, final Cursor cursor) {
+ Assert.isTrue(view instanceof ContactListItemView);
+ final ContactListItemView contactListItemView = (ContactListItemView) view;
+ String alphabetHeader = null;
+ if (mNeedAlphabetHeader) {
+ final int position = cursor.getPosition();
+ final int section = mSectionIndexer.getSectionForPosition(position);
+ // Check if the position is the first in the section.
+ if (mSectionIndexer.getPositionForSection(section) == position) {
+ alphabetHeader = (String) mSectionIndexer.getSections()[section];
+ }
+ }
+ contactListItemView.bind(cursor, mClivHostInterface, mNeedAlphabetHeader, alphabetHeader);
+ }
+
+ @Override
+ public View newView(final Context context, final Cursor cursor, final ViewGroup parent) {
+ final LayoutInflater layoutInflater = LayoutInflater.from(context);
+ return layoutInflater.inflate(R.layout.contact_list_item_view, parent, false);
+ }
+
+ @Override
+ public Cursor swapCursor(final Cursor newCursor) {
+ mSectionIndexer = new ContactSectionIndexer(newCursor);
+ return super.swapCursor(newCursor);
+ }
+
+ @Override
+ public Object[] getSections() {
+ return mSectionIndexer.getSections();
+ }
+
+ @Override
+ public int getPositionForSection(final int sectionIndex) {
+ return mSectionIndexer.getPositionForSection(sectionIndex);
+ }
+
+ @Override
+ public int getSectionForPosition(final int position) {
+ return mSectionIndexer.getSectionForPosition(position);
+ }
+}
diff --git a/src/com/android/messaging/ui/contact/ContactListItemView.java b/src/com/android/messaging/ui/contact/ContactListItemView.java
new file mode 100644
index 0000000..6904da6
--- /dev/null
+++ b/src/com/android/messaging/ui/contact/ContactListItemView.java
@@ -0,0 +1,177 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.messaging.ui.contact;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import com.android.ex.chips.RecipientEntry;
+import com.android.messaging.R;
+import com.android.messaging.datamodel.DataModel;
+import com.android.messaging.datamodel.data.ContactListItemData;
+import com.android.messaging.datamodel.data.ParticipantData;
+import com.android.messaging.ui.ContactIconView;
+import com.android.messaging.util.Assert;
+import com.android.messaging.util.AvatarUriUtil;
+import com.google.common.annotations.VisibleForTesting;
+
+/**
+ * The view for a single entry in a contact list.
+ */
+public class ContactListItemView extends LinearLayout implements OnClickListener {
+ public interface HostInterface {
+ void onContactListItemClicked(ContactListItemData item, ContactListItemView view);
+ boolean isContactSelected(ContactListItemData item);
+ }
+
+ @VisibleForTesting
+ final ContactListItemData mData;
+ private TextView mContactNameTextView;
+ private TextView mContactDetailsTextView;
+ private TextView mContactDetailTypeTextView;
+ private TextView mAlphabetHeaderTextView;
+ private ContactIconView mContactIconView;
+ private ImageView mContactCheckmarkView;
+ private HostInterface mHostInterface;
+ private boolean mShouldShowAlphabetHeader;
+
+ public ContactListItemView(final Context context, final AttributeSet attrs) {
+ super(context, attrs);
+ mData = DataModel.get().createContactListItemData();
+ }
+
+ @Override
+ protected void onFinishInflate () {
+ mContactNameTextView = (TextView) findViewById(R.id.contact_name);
+ mContactDetailsTextView = (TextView) findViewById(R.id.contact_details);
+ mContactDetailTypeTextView = (TextView) findViewById(R.id.contact_detail_type);
+ mAlphabetHeaderTextView = (TextView) findViewById(R.id.alphabet_header);
+ mContactIconView = (ContactIconView) findViewById(R.id.contact_icon);
+ mContactCheckmarkView = (ImageView) findViewById(R.id.contact_checkmark);
+ }
+
+ /**
+ * Fills in the data associated with this view by binding to a contact cursor provided by
+ * ContactUtil.
+ * @param cursor the contact cursor.
+ * @param hostInterface host interface to this view.
+ * @param shouldShowAlphabetHeader whether an alphabetical header should shown on the side
+ * of this view. If {@code headerLabel} is empty, we will still leave space for it.
+ * @param headerLabel the alphabetical header on the side of this view, if it should be shown.
+ */
+ public void bind(final Cursor cursor, final HostInterface hostInterface,
+ final boolean shouldShowAlphabetHeader, final String headerLabel) {
+ mData.bind(cursor, headerLabel);
+ mHostInterface = hostInterface;
+ mShouldShowAlphabetHeader = shouldShowAlphabetHeader;
+ setOnClickListener(this);
+ updateViewAppearance();
+ }
+
+ /**
+ * Binds a RecipientEntry. This is used by the chips text view's dropdown layout.
+ * @param recipientEntry the source RecipientEntry provided by ContactDropdownLayouter, which
+ * was in turn directly from one of the existing chips, or from filtered results
+ * generated by ContactRecipientAdapter.
+ * @param styledName display name where the portion that matches the search text is bold.
+ * @param styledDestination number where the portion that matches the search text is bold.
+ * @param hostInterface host interface to this view.
+ * @param isSingleRecipient whether this item is shown as the only line item in the single
+ * recipient drop down from the chips view. If this is the case, we always show the
+ * contact avatar even if it's not a first-level entry.
+ */
+ public void bind(final RecipientEntry recipientEntry, final CharSequence styledName,
+ final CharSequence styledDestination, final HostInterface hostInterface,
+ final boolean isSingleRecipient) {
+ mData.bind(recipientEntry, styledName, styledDestination, isSingleRecipient);
+ mHostInterface = hostInterface;
+ mShouldShowAlphabetHeader = false;
+ updateViewAppearance();
+ }
+
+ private void updateViewAppearance() {
+ mContactNameTextView.setText(mData.getDisplayName());
+ mContactDetailsTextView.setText(mData.getDestination());
+ mContactDetailTypeTextView.setText(Phone.getTypeLabel(getResources(),
+ mData.getDestinationType(), mData.getDestinationLabel()));
+ final RecipientEntry recipientEntry = mData.getRecipientEntry();
+ final String destinationString = String.valueOf(mData.getDestination());
+ if (mData.getIsSimpleContactItem()) {
+ // This is a special number-with-avatar type of contact (for unknown contact chips
+ // and for direct "send to destination" item). In this case, make sure we only show
+ // the display name (phone number) and the avatar and hide everything else.
+ final Uri avatarUri = AvatarUriUtil.createAvatarUri(
+ ParticipantData.getFromRecipientEntry(recipientEntry));
+ mContactIconView.setImageResourceUri(avatarUri, mData.getContactId(),
+ mData.getLookupKey(), destinationString);
+ mContactIconView.setVisibility(VISIBLE);
+ mContactCheckmarkView.setVisibility(GONE);
+ mContactDetailTypeTextView.setVisibility(GONE);
+ mContactDetailsTextView.setVisibility(GONE);
+ mContactNameTextView.setVisibility(VISIBLE);
+ } else if (mData.getIsFirstLevel()) {
+ final Uri avatarUri = AvatarUriUtil.createAvatarUri(
+ ParticipantData.getFromRecipientEntry(recipientEntry));
+ mContactIconView.setImageResourceUri(avatarUri, mData.getContactId(),
+ mData.getLookupKey(), destinationString);
+ mContactIconView.setVisibility(VISIBLE);
+ mContactNameTextView.setVisibility(VISIBLE);
+ final boolean isSelected = mHostInterface.isContactSelected(mData);
+ setSelected(isSelected);
+ mContactCheckmarkView.setVisibility(isSelected ? VISIBLE : GONE);
+ mContactDetailsTextView.setVisibility(VISIBLE);
+ mContactDetailTypeTextView.setVisibility(VISIBLE);
+ } else {
+ mContactIconView.setImageResourceUri(null);
+ mContactIconView.setVisibility(INVISIBLE);
+ mContactNameTextView.setVisibility(GONE);
+ final boolean isSelected = mHostInterface.isContactSelected(mData);
+ setSelected(isSelected);
+ mContactCheckmarkView.setVisibility(isSelected ? VISIBLE : GONE);
+ mContactDetailsTextView.setVisibility(VISIBLE);
+ mContactDetailTypeTextView.setVisibility(VISIBLE);
+ }
+
+ if (mShouldShowAlphabetHeader) {
+ mAlphabetHeaderTextView.setVisibility(VISIBLE);
+ mAlphabetHeaderTextView.setText(mData.getAlphabetHeader());
+ } else {
+ mAlphabetHeaderTextView.setVisibility(GONE);
+ }
+ }
+
+ /**
+ * {@inheritDoc} from OnClickListener
+ */
+ @Override
+ public void onClick(final View v) {
+ Assert.isTrue(v == this);
+ Assert.isTrue(mHostInterface != null);
+ mHostInterface.onContactListItemClicked(mData, this);
+ }
+
+ public void setImageClickHandlerDisabled(final boolean isHandlerDisabled) {
+ mContactIconView.setImageClickHandlerDisabled(isHandlerDisabled);
+ }
+}
diff --git a/src/com/android/messaging/ui/contact/ContactPickerFragment.java b/src/com/android/messaging/ui/contact/ContactPickerFragment.java
new file mode 100644
index 0000000..d803087
--- /dev/null
+++ b/src/com/android/messaging/ui/contact/ContactPickerFragment.java
@@ -0,0 +1,607 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.messaging.ui.contact;
+
+import android.app.Activity;
+import android.app.Fragment;
+import android.database.Cursor;
+import android.graphics.Rect;
+import android.os.Bundle;
+import android.support.v7.app.ActionBar;
+import android.support.v7.widget.Toolbar;
+import android.support.v7.widget.Toolbar.OnMenuItemClickListener;
+import android.text.Editable;
+import android.text.InputType;
+import android.text.TextUtils;
+import android.text.TextWatcher;
+import android.transition.Explode;
+import android.transition.Transition;
+import android.transition.Transition.EpicenterCallback;
+import android.transition.TransitionManager;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+
+import com.android.messaging.R;
+import com.android.messaging.datamodel.DataModel;
+import com.android.messaging.datamodel.action.ActionMonitor;
+import com.android.messaging.datamodel.action.GetOrCreateConversationAction;
+import com.android.messaging.datamodel.action.GetOrCreateConversationAction.GetOrCreateConversationActionListener;
+import com.android.messaging.datamodel.action.GetOrCreateConversationAction.GetOrCreateConversationActionMonitor;
+import com.android.messaging.datamodel.binding.Binding;
+import com.android.messaging.datamodel.binding.BindingBase;
+import com.android.messaging.datamodel.data.ContactListItemData;
+import com.android.messaging.datamodel.data.ContactPickerData;
+import com.android.messaging.datamodel.data.ContactPickerData.ContactPickerDataListener;
+import com.android.messaging.datamodel.data.ParticipantData;
+import com.android.messaging.ui.CustomHeaderPagerViewHolder;
+import com.android.messaging.ui.CustomHeaderViewPager;
+import com.android.messaging.ui.animation.ViewGroupItemVerticalExplodeAnimation;
+import com.android.messaging.ui.contact.ContactRecipientAutoCompleteView.ContactChipsChangeListener;
+import com.android.messaging.util.Assert;
+import com.android.messaging.util.Assert.RunsOnMainThread;
+import com.android.messaging.util.ContactUtil;
+import com.android.messaging.util.ImeUtil;
+import com.android.messaging.util.LogUtil;
+import com.android.messaging.util.OsUtil;
+import com.android.messaging.util.PhoneUtils;
+import com.android.messaging.util.UiUtils;
+import com.google.common.annotations.VisibleForTesting;
+
+import java.util.ArrayList;
+import java.util.Set;
+
+
+/**
+ * Shows lists of contacts to start conversations with.
+ */
+public class ContactPickerFragment extends Fragment implements ContactPickerDataListener,
+ ContactListItemView.HostInterface, ContactChipsChangeListener, OnMenuItemClickListener,
+ GetOrCreateConversationActionListener {
+ public static final String FRAGMENT_TAG = "contactpicker";
+
+ // Undefined contact picker mode. We should never be in this state after the host activity has
+ // been created.
+ public static final int MODE_UNDEFINED = 0;
+
+ // The initial contact picker mode for starting a new conversation with one contact.
+ public static final int MODE_PICK_INITIAL_CONTACT = 1;
+
+ // The contact picker mode where one initial contact has been picked and we are showing
+ // only the chips edit box.
+ public static final int MODE_CHIPS_ONLY = 2;
+
+ // The contact picker mode for picking more contacts after starting the initial 1-1.
+ public static final int MODE_PICK_MORE_CONTACTS = 3;
+
+ // The contact picker mode when max number of participants is reached.
+ public static final int MODE_PICK_MAX_PARTICIPANTS = 4;
+
+ public interface ContactPickerFragmentHost {
+ void onGetOrCreateNewConversation(String conversationId);
+ void onBackButtonPressed();
+ void onInitiateAddMoreParticipants();
+ void onParticipantCountChanged(boolean canAddMoreParticipants);
+ void invalidateActionBar();
+ }
+
+ @VisibleForTesting
+ final Binding<ContactPickerData> mBinding = BindingBase.createBinding(this);
+
+ private ContactPickerFragmentHost mHost;
+ private ContactRecipientAutoCompleteView mRecipientTextView;
+ private CustomHeaderViewPager mCustomHeaderViewPager;
+ private AllContactsListViewHolder mAllContactsListViewHolder;
+ private FrequentContactsListViewHolder mFrequentContactsListViewHolder;
+ private View mRootView;
+ private View mPendingExplodeView;
+ private View mComposeDivider;
+ private Toolbar mToolbar;
+ private int mContactPickingMode = MODE_UNDEFINED;
+
+ // Keeps track of the currently selected phone numbers in the chips view to enable fast lookup.
+ private Set<String> mSelectedPhoneNumbers = null;
+
+ /**
+ * {@inheritDoc} from Fragment
+ */
+ @Override
+ public void onCreate(final Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ mAllContactsListViewHolder = new AllContactsListViewHolder(getActivity(), this);
+ mFrequentContactsListViewHolder = new FrequentContactsListViewHolder(getActivity(), this);
+
+ if (ContactUtil.hasReadContactsPermission()) {
+ mBinding.bind(DataModel.get().createContactPickerData(getActivity(), this));
+ mBinding.getData().init(getLoaderManager(), mBinding);
+ }
+ }
+
+ /**
+ * {@inheritDoc} from Fragment
+ */
+ @Override
+ public View onCreateView(final LayoutInflater inflater, final ViewGroup container,
+ final Bundle savedInstanceState) {
+ final View view = inflater.inflate(R.layout.contact_picker_fragment, container, false);
+ mRecipientTextView = (ContactRecipientAutoCompleteView)
+ view.findViewById(R.id.recipient_text_view);
+ mRecipientTextView.setThreshold(0);
+ mRecipientTextView.setDropDownAnchor(R.id.compose_contact_divider);
+
+ mRecipientTextView.setContactChipsListener(this);
+ mRecipientTextView.setDropdownChipLayouter(new ContactDropdownLayouter(inflater,
+ getActivity(), this));
+ mRecipientTextView.setAdapter(new ContactRecipientAdapter(getActivity(), this));
+ mRecipientTextView.addTextChangedListener(new TextWatcher() {
+ @Override
+ public void onTextChanged(final CharSequence s, final int start, final int before,
+ final int count) {
+ }
+
+ @Override
+ public void beforeTextChanged(final CharSequence s, final int start, final int count,
+ final int after) {
+ }
+
+ @Override
+ public void afterTextChanged(final Editable s) {
+ updateTextInputButtonsVisibility();
+ }
+ });
+
+ final CustomHeaderPagerViewHolder[] viewHolders = {
+ mFrequentContactsListViewHolder,
+ mAllContactsListViewHolder };
+
+ mCustomHeaderViewPager = (CustomHeaderViewPager) view.findViewById(R.id.contact_pager);
+ mCustomHeaderViewPager.setViewHolders(viewHolders);
+ mCustomHeaderViewPager.setViewPagerTabHeight(CustomHeaderViewPager.DEFAULT_TAB_STRIP_SIZE);
+ mCustomHeaderViewPager.setBackgroundColor(getResources()
+ .getColor(R.color.contact_picker_background));
+
+ // The view pager defaults to the frequent contacts page.
+ mCustomHeaderViewPager.setCurrentItem(0);
+
+ mToolbar = (Toolbar) view.findViewById(R.id.toolbar);
+ mToolbar.setNavigationIcon(R.drawable.ic_arrow_back_light);
+ mToolbar.setNavigationContentDescription(R.string.back);
+ mToolbar.setNavigationOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(final View v) {
+ mHost.onBackButtonPressed();
+ }
+ });
+
+ mToolbar.inflateMenu(R.menu.compose_menu);
+ mToolbar.setOnMenuItemClickListener(this);
+
+ mComposeDivider = view.findViewById(R.id.compose_contact_divider);
+ mRootView = view;
+ return view;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * Called when the host activity has been created. At this point, the host activity should
+ * have set the contact picking mode for us so that we may update our visuals.
+ */
+ @Override
+ public void onActivityCreated(final Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+ Assert.isTrue(mContactPickingMode != MODE_UNDEFINED);
+ updateVisualsForContactPickingMode(false /* animate */);
+ mHost.invalidateActionBar();
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ // We could not have bound to the data if the permission was denied.
+ if (mBinding.isBound()) {
+ mBinding.unbind();
+ }
+
+ if (mMonitor != null) {
+ mMonitor.unregister();
+ }
+ mMonitor = null;
+ }
+
+ @Override
+ public boolean onMenuItemClick(final MenuItem menuItem) {
+ switch (menuItem.getItemId()) {
+ case R.id.action_ime_dialpad_toggle:
+ final int baseInputType = InputType.TYPE_TEXT_FLAG_MULTI_LINE;
+ if ((mRecipientTextView.getInputType() & InputType.TYPE_CLASS_PHONE) !=
+ InputType.TYPE_CLASS_PHONE) {
+ mRecipientTextView.setInputType(baseInputType | InputType.TYPE_CLASS_PHONE);
+ menuItem.setIcon(R.drawable.ic_ime_light);
+ } else {
+ mRecipientTextView.setInputType(baseInputType | InputType.TYPE_CLASS_TEXT);
+ menuItem.setIcon(R.drawable.ic_numeric_dialpad);
+ }
+ ImeUtil.get().showImeKeyboard(getActivity(), mRecipientTextView);
+ return true;
+
+ case R.id.action_add_more_participants:
+ mHost.onInitiateAddMoreParticipants();
+ return true;
+
+ case R.id.action_confirm_participants:
+ maybeGetOrCreateConversation();
+ return true;
+
+ case R.id.action_delete_text:
+ Assert.equals(MODE_PICK_INITIAL_CONTACT, mContactPickingMode);
+ mRecipientTextView.setText("");
+ return true;
+ }
+ return false;
+ }
+
+ @Override // From ContactPickerDataListener
+ public void onAllContactsCursorUpdated(final Cursor data) {
+ mBinding.ensureBound();
+ mAllContactsListViewHolder.onContactsCursorUpdated(data);
+ }
+
+ @Override // From ContactPickerDataListener
+ public void onFrequentContactsCursorUpdated(final Cursor data) {
+ mBinding.ensureBound();
+ mFrequentContactsListViewHolder.onContactsCursorUpdated(data);
+ if (data != null && data.getCount() == 0) {
+ // Show the all contacts list when there's no frequents.
+ mCustomHeaderViewPager.setCurrentItem(1);
+ }
+ }
+
+ @Override // From ContactListItemView.HostInterface
+ public void onContactListItemClicked(final ContactListItemData item,
+ final ContactListItemView view) {
+ if (!isContactSelected(item)) {
+ if (mContactPickingMode == MODE_PICK_INITIAL_CONTACT) {
+ mPendingExplodeView = view;
+ }
+ mRecipientTextView.appendRecipientEntry(item.getRecipientEntry());
+ } else if (mContactPickingMode != MODE_PICK_INITIAL_CONTACT) {
+ mRecipientTextView.removeRecipientEntry(item.getRecipientEntry());
+ }
+ }
+
+ @Override // From ContactListItemView.HostInterface
+ public boolean isContactSelected(final ContactListItemData item) {
+ return mSelectedPhoneNumbers != null &&
+ mSelectedPhoneNumbers.contains(PhoneUtils.getDefault().getCanonicalBySystemLocale(
+ item.getRecipientEntry().getDestination()));
+ }
+
+ /**
+ * Call this immediately after attaching the fragment, or when there's a ui state change that
+ * changes our host (i.e. restore from saved instance state).
+ */
+ public void setHost(final ContactPickerFragmentHost host) {
+ mHost = host;
+ }
+
+ public void setContactPickingMode(final int mode, final boolean animate) {
+ if (mContactPickingMode != mode) {
+ // Guard against impossible transitions.
+ Assert.isTrue(
+ // We may start from undefined mode to any mode when we are restoring state.
+ (mContactPickingMode == MODE_UNDEFINED) ||
+ (mContactPickingMode == MODE_PICK_INITIAL_CONTACT && mode == MODE_CHIPS_ONLY) ||
+ (mContactPickingMode == MODE_CHIPS_ONLY && mode == MODE_PICK_MORE_CONTACTS) ||
+ (mContactPickingMode == MODE_PICK_MORE_CONTACTS
+ && mode == MODE_PICK_MAX_PARTICIPANTS) ||
+ (mContactPickingMode == MODE_PICK_MAX_PARTICIPANTS
+ && mode == MODE_PICK_MORE_CONTACTS));
+
+ mContactPickingMode = mode;
+ updateVisualsForContactPickingMode(animate);
+ }
+ }
+
+ private void showImeKeyboard() {
+ Assert.notNull(mRecipientTextView);
+ mRecipientTextView.requestFocus();
+
+ // showImeKeyboard() won't work until the layout is ready, so wait until layout is complete
+ // before showing the soft keyboard.
+ UiUtils.doOnceAfterLayoutChange(mRootView, new Runnable() {
+ @Override
+ public void run() {
+ final Activity activity = getActivity();
+ if (activity != null) {
+ ImeUtil.get().showImeKeyboard(activity, mRecipientTextView);
+ }
+ }
+ });
+ mRecipientTextView.invalidate();
+ }
+
+ private void updateVisualsForContactPickingMode(final boolean animate) {
+ // Don't update visuals if the visuals haven't been inflated yet.
+ if (mRootView != null) {
+ final Menu menu = mToolbar.getMenu();
+ final MenuItem addMoreParticipantsItem = menu.findItem(
+ R.id.action_add_more_participants);
+ final MenuItem confirmParticipantsItem = menu.findItem(
+ R.id.action_confirm_participants);
+ switch (mContactPickingMode) {
+ case MODE_PICK_INITIAL_CONTACT:
+ addMoreParticipantsItem.setVisible(false);
+ confirmParticipantsItem.setVisible(false);
+ mCustomHeaderViewPager.setVisibility(View.VISIBLE);
+ mComposeDivider.setVisibility(View.INVISIBLE);
+ mRecipientTextView.setEnabled(true);
+ showImeKeyboard();
+ break;
+
+ case MODE_CHIPS_ONLY:
+ if (animate) {
+ if (mPendingExplodeView == null) {
+ // The user didn't click on any contact item, so use the toolbar as
+ // the view to "explode."
+ mPendingExplodeView = mToolbar;
+ }
+ startExplodeTransitionForContactLists(false /* show */);
+
+ ViewGroupItemVerticalExplodeAnimation.startAnimationForView(
+ mCustomHeaderViewPager, mPendingExplodeView, mRootView,
+ true /* snapshotView */, UiUtils.COMPOSE_TRANSITION_DURATION);
+ showHideContactPagerWithAnimation(false /* show */);
+ } else {
+ mCustomHeaderViewPager.setVisibility(View.GONE);
+ }
+
+ addMoreParticipantsItem.setVisible(true);
+ confirmParticipantsItem.setVisible(false);
+ mComposeDivider.setVisibility(View.VISIBLE);
+ mRecipientTextView.setEnabled(true);
+ break;
+
+ case MODE_PICK_MORE_CONTACTS:
+ if (animate) {
+ // Correctly set the start visibility state for the view pager and
+ // individual list items (hidden initially), so that the transition
+ // manager can properly track the visibility change for the explode.
+ mCustomHeaderViewPager.setVisibility(View.VISIBLE);
+ toggleContactListItemsVisibilityForPendingTransition(false /* show */);
+ startExplodeTransitionForContactLists(true /* show */);
+ }
+ addMoreParticipantsItem.setVisible(false);
+ confirmParticipantsItem.setVisible(true);
+ mCustomHeaderViewPager.setVisibility(View.VISIBLE);
+ mComposeDivider.setVisibility(View.INVISIBLE);
+ mRecipientTextView.setEnabled(true);
+ showImeKeyboard();
+ break;
+
+ case MODE_PICK_MAX_PARTICIPANTS:
+ addMoreParticipantsItem.setVisible(false);
+ confirmParticipantsItem.setVisible(true);
+ mCustomHeaderViewPager.setVisibility(View.VISIBLE);
+ mComposeDivider.setVisibility(View.INVISIBLE);
+ // TODO: Verify that this is okay for accessibility
+ mRecipientTextView.setEnabled(false);
+ break;
+
+ default:
+ Assert.fail("Unsupported contact picker mode!");
+ break;
+ }
+ updateTextInputButtonsVisibility();
+ }
+ }
+
+ private void updateTextInputButtonsVisibility() {
+ final Menu menu = mToolbar.getMenu();
+ final MenuItem keypadToggleItem = menu.findItem(R.id.action_ime_dialpad_toggle);
+ final MenuItem deleteTextItem = menu.findItem(R.id.action_delete_text);
+ if (mContactPickingMode == MODE_PICK_INITIAL_CONTACT) {
+ if (TextUtils.isEmpty(mRecipientTextView.getText())) {
+ deleteTextItem.setVisible(false);
+ keypadToggleItem.setVisible(true);
+ } else {
+ deleteTextItem.setVisible(true);
+ keypadToggleItem.setVisible(false);
+ }
+ } else {
+ deleteTextItem.setVisible(false);
+ keypadToggleItem.setVisible(false);
+ }
+ }
+
+ private void maybeGetOrCreateConversation() {
+ final ArrayList<ParticipantData> participants =
+ mRecipientTextView.getRecipientParticipantDataForConversationCreation();
+ if (ContactPickerData.isTooManyParticipants(participants.size())) {
+ UiUtils.showToast(R.string.too_many_participants);
+ } else if (participants.size() > 0 && mMonitor == null) {
+ mMonitor = GetOrCreateConversationAction.getOrCreateConversation(participants,
+ null, this);
+ }
+ }
+
+ /**
+ * Watches changes in contact chips to determine possible state transitions (e.g. creating
+ * the initial conversation, adding more participants or finish the current conversation)
+ */
+ @Override
+ public void onContactChipsChanged(final int oldCount, final int newCount) {
+ Assert.isTrue(oldCount != newCount);
+ if (mContactPickingMode == MODE_PICK_INITIAL_CONTACT) {
+ // Initial picking mode. Start a conversation once a recipient has been picked.
+ maybeGetOrCreateConversation();
+ } else if (mContactPickingMode == MODE_CHIPS_ONLY) {
+ // oldCount == 0 means we are restoring from savedInstanceState to add the existing
+ // chips, don't switch to "add more participants" mode in this case.
+ if (oldCount > 0 && mRecipientTextView.isFocused()) {
+ // Chips only mode. The user may have picked an additional contact or deleted the
+ // only existing contact. Either way, switch to picking more participants mode.
+ mHost.onInitiateAddMoreParticipants();
+ }
+ }
+ mHost.onParticipantCountChanged(ContactPickerData.getCanAddMoreParticipants(newCount));
+
+ // Refresh our local copy of the selected chips set to keep it up-to-date.
+ mSelectedPhoneNumbers = mRecipientTextView.getSelectedDestinations();
+ invalidateContactLists();
+ }
+
+ /**
+ * Listens for notification that invalid contacts have been removed during resolving them.
+ * These contacts were not local contacts, valid email, or valid phone numbers
+ */
+ @Override
+ public void onInvalidContactChipsPruned(final int prunedCount) {
+ Assert.isTrue(prunedCount > 0);
+ UiUtils.showToast(R.plurals.add_invalid_contact_error, prunedCount);
+ }
+
+ /**
+ * Listens for notification that the user has pressed enter/done on the keyboard with all
+ * contacts in place and we should create or go to the existing conversation now
+ */
+ @Override
+ public void onEntryComplete() {
+ if (mContactPickingMode == MODE_PICK_INITIAL_CONTACT ||
+ mContactPickingMode == MODE_PICK_MORE_CONTACTS ||
+ mContactPickingMode == MODE_PICK_MAX_PARTICIPANTS) {
+ // Avoid multiple calls to create in race cases (hit done right after selecting contact)
+ maybeGetOrCreateConversation();
+ }
+ }
+
+ private void invalidateContactLists() {
+ mAllContactsListViewHolder.invalidateList();
+ mFrequentContactsListViewHolder.invalidateList();
+ }
+
+ /**
+ * Kicks off a scene transition that animates visibility changes of individual contact list
+ * items via explode animation.
+ * @param show whether the contact lists are to be shown or hidden.
+ */
+ private void startExplodeTransitionForContactLists(final boolean show) {
+ if (!OsUtil.isAtLeastL()) {
+ // Explode animation is not supported pre-L.
+ return;
+ }
+ final Explode transition = new Explode();
+ final Rect epicenter = mPendingExplodeView == null ? null :
+ UiUtils.getMeasuredBoundsOnScreen(mPendingExplodeView);
+ transition.setDuration(UiUtils.COMPOSE_TRANSITION_DURATION);
+ transition.setInterpolator(UiUtils.EASE_IN_INTERPOLATOR);
+ transition.setEpicenterCallback(new EpicenterCallback() {
+ @Override
+ public Rect onGetEpicenter(final Transition transition) {
+ return epicenter;
+ }
+ });
+
+ // Kick off the delayed scene explode transition. Anything happens after this line in this
+ // method before the next frame will be tracked by the transition manager for visibility
+ // changes and animated accordingly.
+ TransitionManager.beginDelayedTransition(mCustomHeaderViewPager,
+ transition);
+
+ toggleContactListItemsVisibilityForPendingTransition(show);
+ }
+
+ /**
+ * Toggle the visibility of contact list items in the contact lists for them to be tracked by
+ * the transition manager for pending explode transition.
+ */
+ private void toggleContactListItemsVisibilityForPendingTransition(final boolean show) {
+ if (!OsUtil.isAtLeastL()) {
+ // Explode animation is not supported pre-L.
+ return;
+ }
+ mAllContactsListViewHolder.toggleVisibilityForPendingTransition(show, mPendingExplodeView);
+ mFrequentContactsListViewHolder.toggleVisibilityForPendingTransition(show,
+ mPendingExplodeView);
+ }
+
+ private void showHideContactPagerWithAnimation(final boolean show) {
+ final boolean isPagerVisible = (mCustomHeaderViewPager.getVisibility() == View.VISIBLE);
+ if (show == isPagerVisible) {
+ return;
+ }
+
+ mCustomHeaderViewPager.animate().alpha(show ? 1F : 0F)
+ .setStartDelay(!show ? UiUtils.COMPOSE_TRANSITION_DURATION : 0)
+ .withStartAction(new Runnable() {
+ @Override
+ public void run() {
+ mCustomHeaderViewPager.setVisibility(View.VISIBLE);
+ mCustomHeaderViewPager.setAlpha(show ? 0F : 1F);
+ }
+ })
+ .withEndAction(new Runnable() {
+ @Override
+ public void run() {
+ mCustomHeaderViewPager.setVisibility(show ? View.VISIBLE : View.GONE);
+ mCustomHeaderViewPager.setAlpha(1F);
+ }
+ });
+ }
+
+ @Override
+ public void onContactCustomColorLoaded(final ContactPickerData data) {
+ mBinding.ensureBound(data);
+ invalidateContactLists();
+ }
+
+ public void updateActionBar(final ActionBar actionBar) {
+ // Hide the action bar for contact picker mode. The custom ToolBar containing chips UI
+ // etc. will take the spot of the action bar.
+ actionBar.hide();
+ UiUtils.setStatusBarColor(getActivity(),
+ getResources().getColor(R.color.compose_notification_bar_background));
+ }
+
+ private GetOrCreateConversationActionMonitor mMonitor;
+
+ @Override
+ @RunsOnMainThread
+ public void onGetOrCreateConversationSucceeded(final ActionMonitor monitor,
+ final Object data, final String conversationId) {
+ Assert.isTrue(monitor == mMonitor);
+ Assert.isTrue(conversationId != null);
+
+ mRecipientTextView.setInputType(InputType.TYPE_TEXT_FLAG_MULTI_LINE |
+ InputType.TYPE_CLASS_TEXT);
+ mHost.onGetOrCreateNewConversation(conversationId);
+
+ mMonitor = null;
+ }
+
+ @Override
+ @RunsOnMainThread
+ public void onGetOrCreateConversationFailed(final ActionMonitor monitor,
+ final Object data) {
+ Assert.isTrue(monitor == mMonitor);
+ LogUtil.e(LogUtil.BUGLE_TAG, "onGetOrCreateConversationFailed");
+ mMonitor = null;
+ }
+}
diff --git a/src/com/android/messaging/ui/contact/ContactRecipientAdapter.java b/src/com/android/messaging/ui/contact/ContactRecipientAdapter.java
new file mode 100644
index 0000000..25f422e
--- /dev/null
+++ b/src/com/android/messaging/ui/contact/ContactRecipientAdapter.java
@@ -0,0 +1,286 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.messaging.ui.contact;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.database.MergeCursor;
+import android.support.v4.util.Pair;
+import android.text.TextUtils;
+import android.text.util.Rfc822Token;
+import android.text.util.Rfc822Tokenizer;
+import android.widget.Filter;
+
+import com.android.ex.chips.BaseRecipientAdapter;
+import com.android.ex.chips.RecipientAlternatesAdapter;
+import com.android.ex.chips.RecipientAlternatesAdapter.RecipientMatchCallback;
+import com.android.ex.chips.RecipientEntry;
+import com.android.messaging.util.Assert;
+import com.android.messaging.util.Assert.DoesNotRunOnMainThread;
+import com.android.messaging.util.BugleGservices;
+import com.android.messaging.util.BugleGservicesKeys;
+import com.android.messaging.util.ContactRecipientEntryUtils;
+import com.android.messaging.util.ContactUtil;
+import com.android.messaging.util.PhoneUtils;
+
+import java.text.Collator;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+/**
+ * An extension on the base {@link BaseRecipientAdapter} that uses data layer from Bugle,
+ * such as the ContactRecipientPhotoManager that uses our own MediaResourceManager, and
+ * contact lookup that relies on ContactUtil. It provides data source and filtering ability
+ * for {@link ContactRecipientAutoCompleteView}
+ */
+public final class ContactRecipientAdapter extends BaseRecipientAdapter {
+ public ContactRecipientAdapter(final Context context,
+ final ContactListItemView.HostInterface clivHost) {
+ this(context, Integer.MAX_VALUE, QUERY_TYPE_PHONE, clivHost);
+ }
+
+ public ContactRecipientAdapter(final Context context, final int preferredMaxResultCount,
+ final int queryMode, final ContactListItemView.HostInterface clivHost) {
+ super(context, preferredMaxResultCount, queryMode);
+ setPhotoManager(new ContactRecipientPhotoManager(context, clivHost));
+ }
+
+ @Override
+ public boolean forceShowAddress() {
+ // We should always use the SingleRecipientAddressAdapter
+ // And never use the RecipientAlternatesAdapter
+ return true;
+ }
+
+ @Override
+ public Filter getFilter() {
+ return new ContactFilter();
+ }
+
+ /**
+ * A Filter for a RecipientEditTextView that queries Bugle's ContactUtil for auto-complete
+ * results.
+ */
+ public class ContactFilter extends Filter {
+ // Used to sort filtered contacts when it has combined results from email and phone.
+ private final RecipientEntryComparator mComparator = new RecipientEntryComparator();
+
+ /**
+ * Returns a cursor containing the filtered results in contacts given the search text,
+ * and a boolean indicating whether the results are sorted.
+ *
+ * The queries are synchronously performed since this is not run on the main thread.
+ *
+ * Some locales (e.g. JPN) expect email addresses to be auto-completed for MMS.
+ * If this is the case, perform two queries on phone number followed by email and
+ * return the merged results.
+ */
+ @DoesNotRunOnMainThread
+ private Pair<Cursor, Boolean> getFilteredResultsCursor(final Context context,
+ final String searchText) {
+ Assert.isNotMainThread();
+ if (BugleGservices.get().getBoolean(
+ BugleGservicesKeys.ALWAYS_AUTOCOMPLETE_EMAIL_ADDRESS,
+ BugleGservicesKeys.ALWAYS_AUTOCOMPLETE_EMAIL_ADDRESS_DEFAULT)) {
+ return Pair.create((Cursor) new MergeCursor(new Cursor[] {
+ ContactUtil.filterPhones(getContext(), searchText)
+ .performSynchronousQuery(),
+ ContactUtil.filterEmails(getContext(), searchText)
+ .performSynchronousQuery()
+ }), false /* the merged cursor is not sorted */);
+ } else {
+ return Pair.create(ContactUtil.filterDestination(getContext(), searchText)
+ .performSynchronousQuery(), true);
+ }
+ }
+
+ @Override
+ protected FilterResults performFiltering(final CharSequence constraint) {
+ Assert.isNotMainThread();
+ final FilterResults results = new FilterResults();
+
+ // No query, return empty results.
+ if (TextUtils.isEmpty(constraint)) {
+ clearTempEntries();
+ return results;
+ }
+
+ final String searchText = constraint.toString();
+
+ // Query for auto-complete results, since performFiltering() is not done on the
+ // main thread, perform the cursor loader queries directly.
+ final Pair<Cursor, Boolean> filteredResults = getFilteredResultsCursor(getContext(),
+ searchText);
+ final Cursor cursor = filteredResults.first;
+ final boolean sorted = filteredResults.second;
+ if (cursor != null) {
+ try {
+ final List<RecipientEntry> entries = new ArrayList<RecipientEntry>();
+
+ // First check if the constraint is a valid SMS destination. If so, add the
+ // destination as a suggestion item to the drop down.
+ if (PhoneUtils.isValidSmsMmsDestination(searchText)) {
+ entries.add(ContactRecipientEntryUtils
+ .constructSendToDestinationEntry(searchText));
+ }
+
+ HashSet<Long> existingContactIds = new HashSet<Long>();
+ while (cursor.moveToNext()) {
+ // Make sure there's only one first-level contact (i.e. contact for which
+ // we show the avatar picture and name) for every contact id.
+ final long contactId = cursor.getLong(ContactUtil.INDEX_CONTACT_ID);
+ final boolean isFirstLevel = !existingContactIds.contains(contactId);
+ if (isFirstLevel) {
+ existingContactIds.add(contactId);
+ }
+ entries.add(ContactUtil.createRecipientEntryForPhoneQuery(cursor,
+ isFirstLevel));
+ }
+
+ if (!sorted) {
+ Collections.sort(entries, mComparator);
+ }
+ results.values = entries;
+ results.count = 1;
+
+ } finally {
+ cursor.close();
+ }
+ }
+ return results;
+ }
+
+ @Override
+ protected void publishResults(final CharSequence constraint, final FilterResults results) {
+ mCurrentConstraint = constraint;
+ clearTempEntries();
+
+ if (results.values != null) {
+ @SuppressWarnings("unchecked")
+ final List<RecipientEntry> entries = (List<RecipientEntry>) results.values;
+ updateEntries(entries);
+ } else {
+ updateEntries(Collections.<RecipientEntry>emptyList());
+ }
+ }
+
+ private class RecipientEntryComparator implements Comparator<RecipientEntry> {
+ private final Collator mCollator;
+
+ public RecipientEntryComparator() {
+ mCollator = Collator.getInstance(Locale.getDefault());
+ mCollator.setStrength(Collator.PRIMARY);
+ }
+
+ /**
+ * Compare two RecipientEntry's, first by locale-aware display name comparison, then by
+ * contact id comparison, finally by first-level-ness comparison.
+ */
+ @Override
+ public int compare(RecipientEntry lhs, RecipientEntry rhs) {
+ // Send-to-destinations always appear before everything else.
+ final boolean sendToLhs = ContactRecipientEntryUtils
+ .isSendToDestinationContact(lhs);
+ final boolean sendToRhs = ContactRecipientEntryUtils
+ .isSendToDestinationContact(lhs);
+ if (sendToLhs != sendToRhs) {
+ if (sendToLhs) {
+ return -1;
+ } else if (sendToRhs) {
+ return 1;
+ }
+ }
+
+ final int displayNameCompare = mCollator.compare(lhs.getDisplayName(),
+ rhs.getDisplayName());
+ if (displayNameCompare != 0) {
+ return displayNameCompare;
+ }
+
+ // Long.compare could accomplish the following three lines, but this is only
+ // available in API 19+
+ final long lhsContactId = lhs.getContactId();
+ final long rhsContactId = rhs.getContactId();
+ final int contactCompare = lhsContactId < rhsContactId ? -1 :
+ (lhsContactId == rhsContactId ? 0 : 1);
+ if (contactCompare != 0) {
+ return contactCompare;
+ }
+
+ // These are the same contact. Make sure first-level contacts always
+ // appear at the front.
+ if (lhs.isFirstLevel()) {
+ return -1;
+ } else if (rhs.isFirstLevel()) {
+ return 1;
+ } else {
+ return 0;
+ }
+ }
+ }
+ }
+
+ /**
+ * Called when we need to substitute temporary recipient chips with better alternatives.
+ * For example, if a list of comma-delimited phone numbers are pasted into the edit box,
+ * we want to be able to look up in the ContactUtil for exact matches and get contact
+ * details such as name and photo thumbnail for the contact to display a better chip.
+ */
+ @Override
+ public void getMatchingRecipients(final ArrayList<String> inAddresses,
+ final RecipientMatchCallback callback) {
+ final int addressesSize = Math.min(
+ RecipientAlternatesAdapter.MAX_LOOKUPS, inAddresses.size());
+ final HashSet<String> addresses = new HashSet<String>();
+ for (int i = 0; i < addressesSize; i++) {
+ final Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(inAddresses.get(i).toLowerCase());
+ addresses.add(tokens.length > 0 ? tokens[0].getAddress() : inAddresses.get(i));
+ }
+
+ final Map<String, RecipientEntry> recipientEntries =
+ new HashMap<String, RecipientEntry>();
+ // query for each address
+ for (final String address : addresses) {
+ final Cursor cursor = ContactUtil.lookupDestination(getContext(), address)
+ .performSynchronousQuery();
+ if (cursor != null) {
+ try {
+ if (cursor.moveToNext()) {
+ // There may be multiple matches to the same number, always take the
+ // first match.
+ // TODO: May need to consider if there's an existing conversation
+ // that matches this particular contact and prioritize that contact.
+ final RecipientEntry entry =
+ ContactUtil.createRecipientEntryForPhoneQuery(cursor, true);
+ recipientEntries.put(address, entry);
+ }
+
+ } finally {
+ cursor.close();
+ }
+ }
+ }
+
+ // report matches
+ callback.matchesFound(recipientEntries);
+ }
+}
diff --git a/src/com/android/messaging/ui/contact/ContactRecipientAutoCompleteView.java b/src/com/android/messaging/ui/contact/ContactRecipientAutoCompleteView.java
new file mode 100644
index 0000000..c7c2731
--- /dev/null
+++ b/src/com/android/messaging/ui/contact/ContactRecipientAutoCompleteView.java
@@ -0,0 +1,289 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.messaging.ui.contact;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.graphics.Rect;
+import android.os.AsyncTask;
+import android.support.v7.appcompat.R;
+import android.text.Editable;
+import android.text.TextPaint;
+import android.text.TextWatcher;
+import android.text.util.Rfc822Tokenizer;
+import android.util.AttributeSet;
+import android.view.ContextThemeWrapper;
+import android.view.KeyEvent;
+import android.view.inputmethod.EditorInfo;
+import android.widget.TextView;
+
+import com.android.ex.chips.RecipientEditTextView;
+import com.android.ex.chips.RecipientEntry;
+import com.android.ex.chips.recipientchip.DrawableRecipientChip;
+import com.android.messaging.datamodel.data.ParticipantData;
+import com.android.messaging.util.ContactRecipientEntryUtils;
+import com.android.messaging.util.ContactUtil;
+import com.android.messaging.util.PhoneUtils;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+
+/**
+ * An extension for {@link RecipientEditTextView} which shows a list of Materialized contact chips.
+ * It uses Bugle's ContactUtil to perform contact lookup, and is able to return the list of
+ * recipients in the form of a ParticipantData list.
+ */
+public class ContactRecipientAutoCompleteView extends RecipientEditTextView {
+ public interface ContactChipsChangeListener {
+ void onContactChipsChanged(int oldCount, int newCount);
+ void onInvalidContactChipsPruned(int prunedCount);
+ void onEntryComplete();
+ }
+
+ private final int mTextHeight;
+ private ContactChipsChangeListener mChipsChangeListener;
+
+ /**
+ * Watches changes in contact chips to determine possible state transitions.
+ */
+ private class ContactChipsWatcher implements TextWatcher {
+ /**
+ * Tracks the old chips count before text changes. Note that we currently don't compare
+ * the entire chip sets but just the cheaper-to-do before and after counts, because
+ * the chips view don't allow for replacing chips.
+ */
+ private int mLastChipsCount = 0;
+
+ @Override
+ public void onTextChanged(final CharSequence s, final int start, final int before,
+ final int count) {
+ }
+
+ @Override
+ public void beforeTextChanged(final CharSequence s, final int start, final int count,
+ final int after) {
+ // We don't take mLastChipsCount from here but from the last afterTextChanged() run.
+ // The reason is because at this point, any chip spans to be removed is already removed
+ // from s in the chips text view.
+ }
+
+ @Override
+ public void afterTextChanged(final Editable s) {
+ final int currentChipsCount = s.getSpans(0, s.length(),
+ DrawableRecipientChip.class).length;
+ if (currentChipsCount != mLastChipsCount) {
+ // When a sanitizing task is running, we don't want to notify any chips count
+ // change, but we do want to track the last chip count.
+ if (mChipsChangeListener != null && mCurrentSanitizeTask == null) {
+ mChipsChangeListener.onContactChipsChanged(mLastChipsCount, currentChipsCount);
+ }
+ mLastChipsCount = currentChipsCount;
+ }
+ }
+ }
+
+ private static final String TEXT_HEIGHT_SAMPLE = "a";
+
+ public ContactRecipientAutoCompleteView(final Context context, final AttributeSet attrs) {
+ super(new ContextThemeWrapper(context, R.style.ColorAccentGrayOverrideStyle), attrs);
+
+ // Get the height of the text, given the currently set font face and size.
+ final Rect textBounds = new Rect(0, 0, 0, 0);
+ final TextPaint paint = getPaint();
+ paint.getTextBounds(TEXT_HEIGHT_SAMPLE, 0, TEXT_HEIGHT_SAMPLE.length(), textBounds);
+ mTextHeight = textBounds.height();
+
+ setTokenizer(new Rfc822Tokenizer());
+ addTextChangedListener(new ContactChipsWatcher());
+ setOnFocusListShrinkRecipients(false);
+
+ setBackground(context.getResources().getDrawable(
+ R.drawable.abc_textfield_search_default_mtrl_alpha));
+ }
+
+ public void setContactChipsListener(final ContactChipsChangeListener listener) {
+ mChipsChangeListener = listener;
+ }
+
+ /**
+ * A tuple of chips which AsyncContactChipSanitizeTask reports as progress to have the
+ * chip actually replaced/removed on the UI thread.
+ */
+ private class ChipReplacementTuple {
+ public final DrawableRecipientChip removedChip;
+ public final RecipientEntry replacedChipEntry;
+
+ public ChipReplacementTuple(final DrawableRecipientChip removedChip,
+ final RecipientEntry replacedChipEntry) {
+ this.removedChip = removedChip;
+ this.replacedChipEntry = replacedChipEntry;
+ }
+ }
+
+ /**
+ * An AsyncTask that cleans up contact chips on every chips commit (i.e. get or create a new
+ * conversation with the given chips).
+ */
+ private class AsyncContactChipSanitizeTask extends
+ AsyncTask<Void, ChipReplacementTuple, Integer> {
+
+ @Override
+ protected Integer doInBackground(final Void... params) {
+ final DrawableRecipientChip[] recips = getText()
+ .getSpans(0, getText().length(), DrawableRecipientChip.class);
+ int invalidChipsRemoved = 0;
+ for (final DrawableRecipientChip recipient : recips) {
+ final RecipientEntry entry = recipient.getEntry();
+ if (entry != null) {
+ if (entry.isValid()) {
+ if (RecipientEntry.isCreatedRecipient(entry.getContactId()) ||
+ ContactRecipientEntryUtils.isSendToDestinationContact(entry)) {
+ // This is a generated/send-to contact chip, try to look it up and
+ // display a chip for the corresponding local contact.
+ final Cursor lookupResult = ContactUtil.lookupDestination(getContext(),
+ entry.getDestination()).performSynchronousQuery();
+ if (lookupResult != null && lookupResult.moveToNext()) {
+ // Found a match, remove the generated entry and replace with
+ // a better local entry.
+ publishProgress(new ChipReplacementTuple(recipient,
+ ContactUtil.createRecipientEntryForPhoneQuery(
+ lookupResult, true)));
+ } else if (PhoneUtils.isValidSmsMmsDestination(
+ entry.getDestination())){
+ // No match was found, but we have a valid destination so let's at
+ // least create an entry that shows an avatar.
+ publishProgress(new ChipReplacementTuple(recipient,
+ ContactRecipientEntryUtils.constructNumberWithAvatarEntry(
+ entry.getDestination())));
+ } else {
+ // Not a valid contact. Remove and show an error.
+ publishProgress(new ChipReplacementTuple(recipient, null));
+ invalidChipsRemoved++;
+ }
+ }
+ } else {
+ publishProgress(new ChipReplacementTuple(recipient, null));
+ invalidChipsRemoved++;
+ }
+ }
+ }
+ return invalidChipsRemoved;
+ }
+
+ @Override
+ protected void onProgressUpdate(final ChipReplacementTuple... values) {
+ for (final ChipReplacementTuple tuple : values) {
+ if (tuple.removedChip != null) {
+ final Editable text = getText();
+ final int chipStart = text.getSpanStart(tuple.removedChip);
+ final int chipEnd = text.getSpanEnd(tuple.removedChip);
+ if (chipStart >= 0 && chipEnd >= 0) {
+ text.delete(chipStart, chipEnd);
+ }
+
+ if (tuple.replacedChipEntry != null) {
+ appendRecipientEntry(tuple.replacedChipEntry);
+ }
+ }
+ }
+ }
+
+ @Override
+ protected void onPostExecute(final Integer invalidChipsRemoved) {
+ mCurrentSanitizeTask = null;
+ if (invalidChipsRemoved > 0) {
+ mChipsChangeListener.onInvalidContactChipsPruned(invalidChipsRemoved);
+ }
+ }
+ }
+
+ /**
+ * We don't use SafeAsyncTask but instead use a single threaded executor to ensure that
+ * all sanitization tasks are serially executed so as not to interfere with each other.
+ */
+ private static final Executor SANITIZE_EXECUTOR = Executors.newSingleThreadExecutor();
+
+ private AsyncContactChipSanitizeTask mCurrentSanitizeTask;
+
+ /**
+ * Whenever the caller wants to start a new conversation with the list of chips we have,
+ * make sure we asynchronously:
+ * 1. Remove invalid chips.
+ * 2. Attempt to resolve unknown contacts to known local contacts.
+ * 3. Convert still unknown chips to chips with generated avatar.
+ *
+ * Note that we don't need to perform this synchronously since we can
+ * resolve any unknown contacts to local contacts when needed.
+ */
+ private void sanitizeContactChips() {
+ if (mCurrentSanitizeTask != null && !mCurrentSanitizeTask.isCancelled()) {
+ mCurrentSanitizeTask.cancel(false);
+ mCurrentSanitizeTask = null;
+ }
+ mCurrentSanitizeTask = new AsyncContactChipSanitizeTask();
+ mCurrentSanitizeTask.executeOnExecutor(SANITIZE_EXECUTOR);
+ }
+
+ /**
+ * Returns a list of ParticipantData from the entered chips in order to create
+ * new conversation.
+ */
+ public ArrayList<ParticipantData> getRecipientParticipantDataForConversationCreation() {
+ final DrawableRecipientChip[] recips = getText()
+ .getSpans(0, getText().length(), DrawableRecipientChip.class);
+ final ArrayList<ParticipantData> contacts =
+ new ArrayList<ParticipantData>(recips.length);
+ for (final DrawableRecipientChip recipient : recips) {
+ final RecipientEntry entry = recipient.getEntry();
+ if (entry != null && entry.isValid() && entry.getDestination() != null &&
+ PhoneUtils.isValidSmsMmsDestination(entry.getDestination())) {
+ contacts.add(ParticipantData.getFromRecipientEntry(recipient.getEntry()));
+ }
+ }
+ sanitizeContactChips();
+ return contacts;
+ }
+
+ /**c
+ * Gets a set of currently selected chips' emails/phone numbers. This will facilitate the
+ * consumer with determining quickly whether a contact is currently selected.
+ */
+ public Set<String> getSelectedDestinations() {
+ Set<String> set = new HashSet<String>();
+ final DrawableRecipientChip[] recips = getText()
+ .getSpans(0, getText().length(), DrawableRecipientChip.class);
+
+ for (final DrawableRecipientChip recipient : recips) {
+ final RecipientEntry entry = recipient.getEntry();
+ if (entry != null && entry.isValid() && entry.getDestination() != null) {
+ set.add(PhoneUtils.getDefault().getCanonicalBySystemLocale(
+ entry.getDestination()));
+ }
+ }
+ return set;
+ }
+
+ @Override
+ public boolean onEditorAction(final TextView view, final int actionId, final KeyEvent event) {
+ if (actionId == EditorInfo.IME_ACTION_DONE) {
+ mChipsChangeListener.onEntryComplete();
+ }
+ return super.onEditorAction(view, actionId, event);
+ }
+}
diff --git a/src/com/android/messaging/ui/contact/ContactRecipientPhotoManager.java b/src/com/android/messaging/ui/contact/ContactRecipientPhotoManager.java
new file mode 100644
index 0000000..d69ba64
--- /dev/null
+++ b/src/com/android/messaging/ui/contact/ContactRecipientPhotoManager.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.messaging.ui.contact;
+
+import android.content.Context;
+import android.net.Uri;
+
+import com.android.ex.chips.PhotoManager;
+import com.android.ex.chips.RecipientEntry;
+import com.android.messaging.Factory;
+import com.android.messaging.R;
+import com.android.messaging.datamodel.data.ParticipantData;
+import com.android.messaging.datamodel.media.AvatarRequestDescriptor;
+import com.android.messaging.datamodel.media.BindableMediaRequest;
+import com.android.messaging.datamodel.media.ImageResource;
+import com.android.messaging.datamodel.media.MediaRequest;
+import com.android.messaging.datamodel.media.MediaResourceManager;
+import com.android.messaging.datamodel.media.MediaResourceManager.MediaResourceLoadListener;
+import com.android.messaging.util.AvatarUriUtil;
+import com.android.messaging.util.LogUtil;
+import com.android.messaging.util.ThreadUtil;
+
+/**
+ * An implementation of {@link PhotoManager} that hooks up the chips UI's photos with our own
+ * {@link MediaResourceManager} for retrieving and caching contact avatars.
+ */
+public class ContactRecipientPhotoManager implements PhotoManager {
+ private static final String IMAGE_BYTES_REQUEST_STATIC_BINDING_ID = "imagebytes";
+ private final Context mContext;
+ private final int mIconSize;
+ private final ContactListItemView.HostInterface mClivHostInterface;
+
+ public ContactRecipientPhotoManager(final Context context,
+ final ContactListItemView.HostInterface clivHostInterface) {
+ mContext = context;
+ mIconSize = context.getResources().getDimensionPixelSize(
+ R.dimen.compose_message_chip_height) - context.getResources().getDimensionPixelSize(
+ R.dimen.compose_message_chip_padding) * 2;
+ mClivHostInterface = clivHostInterface;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void populatePhotoBytesAsync(final RecipientEntry entry,
+ final PhotoManagerCallback callback) {
+ // Post all media resource request to the main thread.
+ ThreadUtil.getMainThreadHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ final Uri avatarUri = AvatarUriUtil.createAvatarUri(
+ ParticipantData.getFromRecipientEntry(entry));
+ final AvatarRequestDescriptor descriptor =
+ new AvatarRequestDescriptor(avatarUri, mIconSize, mIconSize);
+ final BindableMediaRequest<ImageResource> req = descriptor.buildAsyncMediaRequest(
+ mContext,
+ new MediaResourceLoadListener<ImageResource>() {
+ @Override
+ public void onMediaResourceLoaded(final MediaRequest<ImageResource> request,
+ final ImageResource resource, final boolean isCached) {
+ entry.setPhotoBytes(resource.getBytes());
+ callback.onPhotoBytesAsynchronouslyPopulated();
+ }
+
+ @Override
+ public void onMediaResourceLoadError(final MediaRequest<ImageResource> request,
+ final Exception exception) {
+ LogUtil.e(LogUtil.BUGLE_TAG, "Photo bytes loading failed due to " +
+ exception + " request key=" + request.getKey());
+
+ // Fall back to the default avatar image.
+ callback.onPhotoBytesAsyncLoadFailed();
+ }});
+
+ // Statically bind the request since it's not bound to any specific piece of UI.
+ req.bind(IMAGE_BYTES_REQUEST_STATIC_BINDING_ID);
+
+ Factory.get().getMediaResourceManager().requestMediaResourceAsync(req);
+ }
+ });
+ }
+}
diff --git a/src/com/android/messaging/ui/contact/ContactSectionIndexer.java b/src/com/android/messaging/ui/contact/ContactSectionIndexer.java
new file mode 100644
index 0000000..1d5abf3
--- /dev/null
+++ b/src/com/android/messaging/ui/contact/ContactSectionIndexer.java
@@ -0,0 +1,169 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.messaging.ui.contact;
+
+import android.database.Cursor;
+import android.os.Bundle;
+import android.provider.ContactsContract.Contacts;
+import android.text.TextUtils;
+import android.widget.SectionIndexer;
+
+import com.android.messaging.util.Assert;
+import com.android.messaging.util.ContactUtil;
+import com.android.messaging.util.LogUtil;
+
+import java.util.ArrayList;
+
+/**
+ * Indexes contact alphabetical sections so we can report to the fast scrolling list view
+ * where we are in the list when the user scrolls through the contact list, allowing us to show
+ * alphabetical indicators for the fast scroller as well as list section headers.
+ */
+public class ContactSectionIndexer implements SectionIndexer {
+ private String[] mSections;
+ private ArrayList<Integer> mSectionStartingPositions;
+ private static final String BLANK_HEADER_STRING = " ";
+
+ public ContactSectionIndexer(final Cursor contactsCursor) {
+ buildIndexer(contactsCursor);
+ }
+
+ @Override
+ public Object[] getSections() {
+ return mSections;
+ }
+
+ @Override
+ public int getPositionForSection(final int sectionIndex) {
+ if (mSectionStartingPositions.isEmpty()) {
+ return 0;
+ }
+ // Clamp to the bounds of the section position array per Android API doc.
+ return mSectionStartingPositions.get(
+ Math.max(Math.min(sectionIndex, mSectionStartingPositions.size() - 1), 0));
+ }
+
+ @Override
+ public int getSectionForPosition(final int position) {
+ if (mSectionStartingPositions.isEmpty()) {
+ return 0;
+ }
+
+ // Perform a binary search on the starting positions of the sections to the find the
+ // section for the position.
+ int left = 0;
+ int right = mSectionStartingPositions.size() - 1;
+
+ // According to getSectionForPosition()'s doc, we should always clamp the value when the
+ // position is out of bound.
+ if (position <= mSectionStartingPositions.get(left)) {
+ return left;
+ } else if (position >= mSectionStartingPositions.get(right)) {
+ return right;
+ }
+
+ while (left <= right) {
+ final int mid = (left + right) / 2;
+ final int startingPos = mSectionStartingPositions.get(mid);
+ final int nextStartingPos = mSectionStartingPositions.get(mid + 1);
+ if (position >= startingPos && position < nextStartingPos) {
+ return mid;
+ } else if (position < startingPos) {
+ right = mid - 1;
+ } else if (position >= nextStartingPos) {
+ left = mid + 1;
+ }
+ }
+ Assert.fail("Invalid section indexer state: couldn't find section for pos " + position);
+ return -1;
+ }
+
+ private boolean buildIndexerFromCursorExtras(final Cursor cursor) {
+ if (cursor == null) {
+ return false;
+ }
+ final Bundle cursorExtras = cursor.getExtras();
+ if (cursorExtras == null) {
+ return false;
+ }
+ final String[] sections = cursorExtras.getStringArray(
+ Contacts.EXTRA_ADDRESS_BOOK_INDEX_TITLES);
+ final int[] counts = cursorExtras.getIntArray(Contacts.EXTRA_ADDRESS_BOOK_INDEX_COUNTS);
+ if (sections == null || counts == null) {
+ return false;
+ }
+
+ if (sections.length != counts.length) {
+ return false;
+ }
+
+ this.mSections = sections;
+ mSectionStartingPositions = new ArrayList<Integer>(counts.length);
+ int position = 0;
+ for (int i = 0; i < counts.length; i++) {
+ if (TextUtils.isEmpty(mSections[i])) {
+ mSections[i] = BLANK_HEADER_STRING;
+ } else if (!mSections[i].equals(BLANK_HEADER_STRING)) {
+ mSections[i] = mSections[i].trim();
+ }
+
+ mSectionStartingPositions.add(position);
+ position += counts[i];
+ }
+ return true;
+ }
+
+ private void buildIndexerFromDisplayNames(final Cursor cursor) {
+ // Loop through the contact cursor and get the starting position for each first character.
+ // The result is stored into two arrays, one for the section header (i.e. the first
+ // character), and one for the starting position, which is guaranteed to be sorted in
+ // ascending order.
+ final ArrayList<String> sections = new ArrayList<String>();
+ mSectionStartingPositions = new ArrayList<Integer>();
+ if (cursor != null) {
+ cursor.moveToPosition(-1);
+ int currentPosition = 0;
+ while (cursor.moveToNext()) {
+ // The sort key is typically the contact's display name, so for example, a contact
+ // named "Bob" will go into section "B". The Contacts provider generally uses a
+ // a slightly more sophisticated heuristic, but as a fallback this is good enough.
+ final String sortKey = cursor.getString(ContactUtil.INDEX_SORT_KEY);
+ final String section = TextUtils.isEmpty(sortKey) ? BLANK_HEADER_STRING :
+ sortKey.substring(0, 1).toUpperCase();
+
+ final int lastIndex = sections.size() - 1;
+ final String currentSection = lastIndex >= 0 ? sections.get(lastIndex) : null;
+ if (!TextUtils.equals(currentSection, section)) {
+ sections.add(section);
+ mSectionStartingPositions.add(currentPosition);
+ }
+ currentPosition++;
+ }
+ }
+ mSections = new String[sections.size()];
+ sections.toArray(mSections);
+ }
+
+ private void buildIndexer(final Cursor cursor) {
+ // First check if we get indexer label extras from the contact provider; if not, fall back
+ // to building from display names.
+ if (!buildIndexerFromCursorExtras(cursor)) {
+ LogUtil.w(LogUtil.BUGLE_TAG, "contact provider didn't provide contact label " +
+ "information, fall back to using display name!");
+ buildIndexerFromDisplayNames(cursor);
+ }
+ }
+}
diff --git a/src/com/android/messaging/ui/contact/FrequentContactsListViewHolder.java b/src/com/android/messaging/ui/contact/FrequentContactsListViewHolder.java
new file mode 100644
index 0000000..1f3c795
--- /dev/null
+++ b/src/com/android/messaging/ui/contact/FrequentContactsListViewHolder.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.messaging.ui.contact;
+
+import android.content.Context;
+
+import com.android.messaging.R;
+import com.android.messaging.ui.CustomHeaderPagerListViewHolder;
+import com.android.messaging.ui.contact.ContactListItemView.HostInterface;
+
+/**
+ * Holds the frequent contacts view for the contact picker's view pager.
+ */
+public class FrequentContactsListViewHolder extends CustomHeaderPagerListViewHolder {
+ public FrequentContactsListViewHolder(final Context context,
+ final HostInterface clivHostInterface) {
+ super(context, new ContactListAdapter(context, null, clivHostInterface,
+ false /* needAlphabetHeader */));
+ }
+
+ @Override
+ protected int getLayoutResId() {
+ return R.layout.frequent_contacts_list_view;
+ }
+
+ @Override
+ protected int getPageTitleResId() {
+ return R.string.contact_picker_frequents_tab_title;
+ }
+
+ @Override
+ protected int getEmptyViewResId() {
+ return R.id.empty_view;
+ }
+
+ @Override
+ protected int getListViewResId() {
+ return R.id.frequent_contacts_list;
+ }
+
+ @Override
+ protected int getEmptyViewTitleResId() {
+ return R.string.contact_list_empty_text;
+ }
+
+ @Override
+ protected int getEmptyViewImageResId() {
+ return R.drawable.ic_oobe_freq_list;
+ }
+}