Revert "Initial checkin of AOSP Messaging app."

This reverts commit 461a34b466.

Change-Id: Iac4ca77eeaa94989e91dead49a7959c905bd3078
This commit is contained in:
Mike Dodd
2015-08-12 18:48:39 +00:00
parent 461a34b466
commit ef8c7abbcf
1645 changed files with 0 additions and 186271 deletions
@@ -1,85 +0,0 @@
/*
* 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;
}
}
@@ -1,62 +0,0 @@
/*
* 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;
}
}
@@ -1,138 +0,0 @@
/*
* 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);
}
}
@@ -1,86 +0,0 @@
/*
* 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);
}
}
@@ -1,177 +0,0 @@
/*
* 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);
}
}
@@ -1,607 +0,0 @@
/*
* 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;
}
}
@@ -1,286 +0,0 @@
/*
* 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);
}
}
@@ -1,289 +0,0 @@
/*
* 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);
}
}
@@ -1,96 +0,0 @@
/*
* 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);
}
});
}
}
@@ -1,169 +0,0 @@
/*
* 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);
}
}
}
@@ -1,63 +0,0 @@
/*
* 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;
}
}