Revert "Initial checkin of AOSP Messaging app."
This reverts commit 461a34b466.
Change-Id: Iac4ca77eeaa94989e91dead49a7959c905bd3078
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user