/* * 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.contacts.editor; import com.android.contacts.R; import com.android.contacts.common.model.AccountTypeManager; import com.android.contacts.common.model.RawContactDelta; import com.android.contacts.common.model.RawContactDeltaList; import com.android.contacts.common.model.RawContactModifier; import com.android.contacts.common.model.ValuesDelta; import com.android.contacts.common.model.account.AccountType; import com.android.contacts.common.model.account.AccountWithDataSet; import com.android.contacts.common.model.dataitem.DataKind; import com.android.contacts.common.util.AccountsListAdapter; import com.android.contacts.common.util.MaterialColorMapUtils; import com.android.contacts.util.UiClosables; import android.content.ContentUris; import android.content.Context; import android.database.Cursor; import android.graphics.Bitmap; import android.net.Uri; import android.os.Bundle; import android.os.Parcel; import android.os.Parcelable; import android.provider.ContactsContract; import android.provider.ContactsContract.CommonDataKinds.Email; import android.provider.ContactsContract.CommonDataKinds.Event; import android.provider.ContactsContract.CommonDataKinds.GroupMembership; import android.provider.ContactsContract.CommonDataKinds.Im; import android.provider.ContactsContract.CommonDataKinds.Nickname; import android.provider.ContactsContract.CommonDataKinds.Note; import android.provider.ContactsContract.CommonDataKinds.Organization; import android.provider.ContactsContract.CommonDataKinds.Phone; import android.provider.ContactsContract.CommonDataKinds.Photo; import android.provider.ContactsContract.CommonDataKinds.Relation; import android.provider.ContactsContract.CommonDataKinds.SipAddress; import android.provider.ContactsContract.CommonDataKinds.StructuredName; import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; import android.provider.ContactsContract.CommonDataKinds.Website; import android.text.TextUtils; import android.util.AttributeSet; import android.util.Log; import android.util.Pair; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.AdapterView; import android.widget.BaseAdapter; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.ListPopupWindow; import android.widget.TextView; import java.io.FileNotFoundException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.TreeSet; /** * View to display information from multiple {@link RawContactDelta}s grouped together. */ public class CompactRawContactsEditorView extends LinearLayout implements View.OnClickListener { static final String TAG = "CompactEditorView"; private static final KindSectionDataMapEntryComparator KIND_SECTION_DATA_MAP_ENTRY_COMPARATOR = new KindSectionDataMapEntryComparator(); /** * Callbacks for hosts of {@link CompactRawContactsEditorView}s. */ public interface Listener { /** * Invoked when the structured name editor field has changed. * * @param rawContactId The raw contact ID from the underlying {@link RawContactDelta}. * @param valuesDelta The values from the underlying {@link RawContactDelta}. */ public void onNameFieldChanged(long rawContactId, ValuesDelta valuesDelta); /** * Invoked when the compact editor should rebind editors for a new account. * * @param oldState Old data being edited. * @param oldAccount Old account associated with oldState. * @param newAccount New account to be used. */ public void onRebindEditorsForNewContact(RawContactDelta oldState, AccountWithDataSet oldAccount, AccountWithDataSet newAccount); /** * Invoked when no editors could be bound for the contact. */ public void onBindEditorsFailed(); /** * Invoked after editors have been bound for the contact. */ public void onEditorsBound(); /** * Invoked when a rawcontact from linked contacts is selected in editor. */ public void onRawContactSelected(Uri uri, long rawContactId, boolean isReadOnly); /** * Returns the map of raw contact IDs to newly taken or selected photos that have not * yet been saved to CP2. */ public Bundle getUpdatedPhotos(); } /** * Used to list the account info for the given raw contacts list. */ private static final class RawContactAccountListAdapter extends BaseAdapter { private final LayoutInflater mInflater; private final Context mContext; private final RawContactDeltaList mRawContactDeltas; public RawContactAccountListAdapter(Context context, RawContactDeltaList rawContactDeltas) { mContext = context; mRawContactDeltas = new RawContactDeltaList(); for (RawContactDelta rawContactDelta : rawContactDeltas) { if (rawContactDelta.isVisible() && rawContactDelta.getRawContactId() > 0) { mRawContactDeltas.add(rawContactDelta); } } mInflater = LayoutInflater.from(context); } @Override public View getView(int position, View convertView, ViewGroup parent) { final View resultView = convertView != null ? convertView : mInflater.inflate(R.layout.account_selector_list_item, parent, false); final RawContactDelta rawContactDelta = mRawContactDeltas.get(position); final TextView text1 = (TextView) resultView.findViewById(android.R.id.text1); final AccountType accountType = rawContactDelta.getRawContactAccountType(mContext); text1.setText(accountType.getDisplayLabel(mContext)); final TextView text2 = (TextView) resultView.findViewById(android.R.id.text2); final String accountName = rawContactDelta.getAccountName(); if (TextUtils.isEmpty(accountName)) { text2.setVisibility(View.GONE); } else { // Truncate email addresses in the middle so we don't lose the domain text2.setText(accountName); text2.setEllipsize(TextUtils.TruncateAt.MIDDLE); } final ImageView icon = (ImageView) resultView.findViewById(android.R.id.icon); icon.setImageDrawable(accountType.getDisplayIcon(mContext)); return resultView; } @Override public int getCount() { return mRawContactDeltas.size(); } @Override public RawContactDelta getItem(int position) { return mRawContactDeltas.get(position); } @Override public long getItemId(int position) { return getItem(position).getRawContactId(); } } /** Used to sort entire kind sections. */ private static final class KindSectionDataMapEntryComparator implements Comparator<Map.Entry<String,KindSectionDataList>> { final MimeTypeComparator mMimeTypeComparator = new MimeTypeComparator(); @Override public int compare(Map.Entry<String, KindSectionDataList> entry1, Map.Entry<String, KindSectionDataList> entry2) { if (entry1 == entry2) return 0; if (entry1 == null) return -1; if (entry2 == null) return 1; final String mimeType1 = entry1.getKey(); final String mimeType2 = entry2.getKey(); return mMimeTypeComparator.compare(mimeType1, mimeType2); } } /** * Sorts kinds roughly the same as quick contacts; we diverge in the following ways: * <ol> * <li>All names are together at the top.</li> * <li>IM is moved up after addresses</li> * <li>SIP addresses are moved to below phone numbers</li> * <li>Group membership is placed at the end</li> * </ol> */ private static final class MimeTypeComparator implements Comparator<String> { private static final List<String> MIME_TYPE_ORDER = Arrays.asList(new String[] { StructuredName.CONTENT_ITEM_TYPE, Nickname.CONTENT_ITEM_TYPE, Organization.CONTENT_ITEM_TYPE, Phone.CONTENT_ITEM_TYPE, SipAddress.CONTENT_ITEM_TYPE, Email.CONTENT_ITEM_TYPE, StructuredPostal.CONTENT_ITEM_TYPE, Im.CONTENT_ITEM_TYPE, Website.CONTENT_ITEM_TYPE, Event.CONTENT_ITEM_TYPE, Relation.CONTENT_ITEM_TYPE, Note.CONTENT_ITEM_TYPE, GroupMembership.CONTENT_ITEM_TYPE }); @Override public int compare(String mimeType1, String mimeType2) { if (mimeType1 == mimeType2) return 0; if (mimeType1 == null) return -1; if (mimeType2 == null) return 1; int index1 = MIME_TYPE_ORDER.indexOf(mimeType1); int index2 = MIME_TYPE_ORDER.indexOf(mimeType2); // Fallback to alphabetical ordering of the mime type if both are not found if (index1 < 0 && index2 < 0) return mimeType1.compareTo(mimeType2); if (index1 < 0) return 1; if (index2 < 0) return -1; return index1 < index2 ? -1 : 1; } } /** * Sorts primary accounts and google account types before others. */ private static final class EditorComparator implements Comparator<KindSectionData> { private RawContactDeltaComparator mRawContactDeltaComparator; private EditorComparator(Context context) { mRawContactDeltaComparator = new RawContactDeltaComparator(context); } @Override public int compare(KindSectionData kindSectionData1, KindSectionData kindSectionData2) { if (kindSectionData1 == kindSectionData2) return 0; if (kindSectionData1 == null) return -1; if (kindSectionData2 == null) return 1; final RawContactDelta rawContactDelta1 = kindSectionData1.getRawContactDelta(); final RawContactDelta rawContactDelta2 = kindSectionData2.getRawContactDelta(); if (rawContactDelta1 == rawContactDelta2) return 0; if (rawContactDelta1 == null) return -1; if (rawContactDelta2 == null) return 1; return mRawContactDeltaComparator.compare(rawContactDelta1, rawContactDelta2); } } public static class SavedState extends BaseSavedState { public static final Parcelable.Creator<SavedState> CREATOR = new Parcelable.Creator<SavedState>() { public SavedState createFromParcel(Parcel in) { return new SavedState(in); } public SavedState[] newArray(int size) { return new SavedState[size]; } }; private boolean mIsExpanded; public SavedState(Parcelable superState) { super(superState); } private SavedState(Parcel in) { super(in); mIsExpanded = in.readInt() != 0; } @Override public void writeToParcel(Parcel out, int flags) { super.writeToParcel(out, flags); out.writeInt(mIsExpanded ? 1 : 0); } } private CompactRawContactsEditorView.Listener mListener; private AccountTypeManager mAccountTypeManager; private LayoutInflater mLayoutInflater; private ViewIdGenerator mViewIdGenerator; private MaterialColorMapUtils.MaterialPalette mMaterialPalette; private long mPhotoId = -1; private boolean mHasNewContact; private boolean mIsUserProfile; private AccountWithDataSet mPrimaryAccount; private Map<String,KindSectionDataList> mKindSectionDataMap = new HashMap<>(); // Account header private View mAccountHeaderContainer; private TextView mAccountHeaderType; private TextView mAccountHeaderName; private ImageView mAccountHeaderIcon; // Account selector private View mAccountSelectorContainer; private View mAccountSelector; private TextView mAccountSelectorType; private TextView mAccountSelectorName; // Raw contacts selector private View mRawContactContainer; private TextView mRawContactSummary; private CompactPhotoEditorView mPhotoView; private ViewGroup mKindSectionViews; private Map<String,List<CompactKindSectionView>> mKindSectionViewsMap = new HashMap<>(); private View mMoreFields; private boolean mIsExpanded; private long mPhotoRawContactId; private ValuesDelta mPhotoValuesDelta; private Pair<KindSectionData, ValuesDelta> mPrimaryNameKindSectionData; public CompactRawContactsEditorView(Context context) { super(context); } public CompactRawContactsEditorView(Context context, AttributeSet attrs) { super(context, attrs); } /** * Sets the receiver for {@link CompactRawContactsEditorView} callbacks. */ public void setListener(Listener listener) { mListener = listener; } @Override protected void onFinishInflate() { super.onFinishInflate(); mAccountTypeManager = AccountTypeManager.getInstance(getContext()); mLayoutInflater = (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE); // Account header mAccountHeaderContainer = findViewById(R.id.account_container); mAccountHeaderType = (TextView) findViewById(R.id.account_type); mAccountHeaderName = (TextView) findViewById(R.id.account_name); mAccountHeaderIcon = (ImageView) findViewById(R.id.account_type_icon); // Account selector mAccountSelectorContainer = findViewById(R.id.account_selector_container); mAccountSelector = findViewById(R.id.account); mAccountSelectorType = (TextView) findViewById(R.id.account_type_selector); mAccountSelectorName = (TextView) findViewById(R.id.account_name_selector); // Raw contacts selector mRawContactContainer = findViewById(R.id.all_rawcontacts_accounts_container); mRawContactSummary = (TextView) findViewById(R.id.rawcontacts_accounts_summary); mPhotoView = (CompactPhotoEditorView) findViewById(R.id.photo_editor); mKindSectionViews = (LinearLayout) findViewById(R.id.kind_section_views); mMoreFields = findViewById(R.id.more_fields); mMoreFields.setOnClickListener(this); } @Override public void onClick(View view) { if (view.getId() == R.id.more_fields) { showAllFields(); } } @Override public void setEnabled(boolean enabled) { super.setEnabled(enabled); final int childCount = mKindSectionViews.getChildCount(); for (int i = 0; i < childCount; i++) { mKindSectionViews.getChildAt(i).setEnabled(enabled); } } @Override public Parcelable onSaveInstanceState() { final Parcelable superState = super.onSaveInstanceState(); final SavedState savedState = new SavedState(superState); savedState.mIsExpanded = mIsExpanded; return savedState; } @Override public void onRestoreInstanceState(Parcelable state) { if(!(state instanceof SavedState)) { super.onRestoreInstanceState(state); return; } final SavedState savedState = (SavedState) state; super.onRestoreInstanceState(savedState.getSuperState()); mIsExpanded = savedState.mIsExpanded; if (mIsExpanded) { showAllFields(); } } /** * Pass through to {@link CompactPhotoEditorView#setListener}. */ public void setPhotoListener(CompactPhotoEditorView.Listener listener) { mPhotoView.setListener(listener); } public void removePhoto() { mPhotoValuesDelta.setFromTemplate(true); mPhotoValuesDelta.put(Photo.PHOTO, (byte[]) null); mPhotoView.removePhoto(); } /** * Pass through to {@link CompactPhotoEditorView#setFullSizedPhoto(Uri)}. */ public void setFullSizePhoto(Uri photoUri) { mPhotoView.setFullSizedPhoto(photoUri); } public void updatePhoto(Uri photoUri) { mPhotoValuesDelta.setFromTemplate(false); // Unset primary for all photos unsetSuperPrimaryFromAllPhotos(); // Mark the currently displayed photo as primary mPhotoValuesDelta.setSuperPrimary(true); // Even though high-res photos cannot be saved by passing them via // an EntityDeltaList (since they cause the Bundle size limit to be // exceeded), we still pass a low-res thumbnail. This simplifies // code all over the place, because we don't have to test whether // there is a change in EITHER the delta-list OR a changed photo... // this way, there is always a change in the delta-list. try { final byte[] bytes = EditorUiUtils.getCompressedThumbnailBitmapBytes( getContext(), photoUri); if (bytes != null) { mPhotoValuesDelta.setPhoto(bytes); } } catch (FileNotFoundException e) { elog("Failed to get bitmap from photo Uri"); } mPhotoView.setFullSizedPhoto(photoUri); } private void unsetSuperPrimaryFromAllPhotos() { final List<KindSectionData> kindSectionDataList = mKindSectionDataMap.get(Photo.CONTENT_ITEM_TYPE); for (KindSectionData kindSectionData : kindSectionDataList) { for (ValuesDelta valuesDelta : kindSectionData.getNonEmptyValuesDeltas()) { valuesDelta.setSuperPrimary(false); } } } /** * Pass through to {@link CompactPhotoEditorView#isWritablePhotoSet}. */ public boolean isWritablePhotoSet() { return mPhotoView.isWritablePhotoSet(); } /** * Get the raw contact ID for the CompactHeaderView photo. */ public long getPhotoRawContactId() { return mPhotoRawContactId; } public StructuredNameEditorView getPrimaryNameEditorView() { final CompactKindSectionView primaryNameKindSectionView = getPrimaryNameKindSectionView(); return primaryNameKindSectionView == null ? null : primaryNameKindSectionView.getPrimaryNameEditorView(); } /** * Returns a data holder for every non-default/non-empty photo from each raw contact, whether * the raw contact is writable or not. */ public ArrayList<CompactPhotoSelectionFragment.Photo> getPhotos() { final ArrayList<CompactPhotoSelectionFragment.Photo> photos = new ArrayList<>(); final Bundle updatedPhotos = mListener == null ? null : mListener.getUpdatedPhotos(); final List<KindSectionData> kindSectionDataList = mKindSectionDataMap.get(Photo.CONTENT_ITEM_TYPE); for (int i = 0; i < kindSectionDataList.size(); i++) { final KindSectionData kindSectionData = kindSectionDataList.get(i); final AccountType accountType = kindSectionData.getAccountType(); final List<ValuesDelta> valuesDeltas = kindSectionData.getNonEmptyValuesDeltas(); if (valuesDeltas.isEmpty()) continue; for (int j = 0; j < valuesDeltas.size(); j++) { final ValuesDelta valuesDelta = valuesDeltas.get(j); final Bitmap bitmap = EditorUiUtils.getPhotoBitmap(valuesDelta); if (bitmap == null) continue; final CompactPhotoSelectionFragment.Photo photo = new CompactPhotoSelectionFragment.Photo(); photo.titleRes = accountType.titleRes; photo.iconRes = accountType.iconRes; photo.syncAdapterPackageName = accountType.syncAdapterPackageName; photo.valuesDelta = valuesDelta; photo.primary = valuesDelta.isSuperPrimary(); photo.kindSectionDataListIndex = i; photo.valuesDeltaListIndex = j; photo.photoId = valuesDelta.getId(); if (updatedPhotos != null) { photo.updatedPhotoUri = (Uri) updatedPhotos.get(String.valueOf( kindSectionData.getRawContactDelta().getRawContactId())); } final CharSequence accountTypeLabel = accountType.getDisplayLabel(getContext()); photo.accountType = accountTypeLabel == null ? "" : accountTypeLabel.toString(); final String accountName = kindSectionData.getRawContactDelta().getAccountName(); photo.accountName = accountName == null ? "" : accountName; photos.add(photo); } } return photos; } /** * Marks the raw contact photo given as primary for the aggregate contact and updates the * UI. */ public void setPrimaryPhoto(CompactPhotoSelectionFragment.Photo photo) { // Find the values delta to mark as primary final KindSectionDataList kindSectionDataList = mKindSectionDataMap.get(Photo.CONTENT_ITEM_TYPE); if (photo.kindSectionDataListIndex < 0 || photo.kindSectionDataListIndex >= kindSectionDataList.size()) { wlog("Invalid kind section data list index"); return; } final KindSectionData kindSectionData = kindSectionDataList.get(photo.kindSectionDataListIndex); final List<ValuesDelta> valuesDeltaList = kindSectionData.getNonEmptyValuesDeltas(); if (photo.valuesDeltaListIndex >= valuesDeltaList.size()) { wlog("Invalid values delta list index"); return; } // Update values delta final ValuesDelta valuesDelta = valuesDeltaList.get(photo.valuesDeltaListIndex); valuesDelta.setFromTemplate(false); unsetSuperPrimaryFromAllPhotos(); valuesDelta.setSuperPrimary(true); // Update the UI mPhotoView.setPhoto(valuesDelta, mMaterialPalette); } public View getAggregationAnchorView() { final List<CompactKindSectionView> kindSectionViews = getKindSectionViews( StructuredName.CONTENT_ITEM_TYPE); if (!kindSectionViews.isEmpty()) { return mKindSectionViews.getChildAt(0).findViewById(R.id.anchor_view); } return null; } public void setGroupMetaData(Cursor groupMetaData) { final List<CompactKindSectionView> kindSectionViews = getKindSectionViews( GroupMembership.CONTENT_ITEM_TYPE); for (CompactKindSectionView kindSectionView : kindSectionViews) { kindSectionView.setGroupMetaData(groupMetaData); if (mIsExpanded) { kindSectionView.setHideWhenEmpty(false); kindSectionView.updateEmptyEditors(/* shouldAnimate =*/ true); } } } public void setState(RawContactDeltaList rawContactDeltas, MaterialColorMapUtils.MaterialPalette materialPalette, ViewIdGenerator viewIdGenerator, long photoId, boolean hasNewContact, boolean isUserProfile, AccountWithDataSet primaryAccount) { mKindSectionDataMap.clear(); mKindSectionViews.removeAllViews(); mMoreFields.setVisibility(View.VISIBLE); mMaterialPalette = materialPalette; mViewIdGenerator = viewIdGenerator; mPhotoId = photoId; mHasNewContact = hasNewContact; mIsUserProfile = isUserProfile; mPrimaryAccount = primaryAccount; if (mPrimaryAccount == null) { mPrimaryAccount = ContactEditorUtils.getInstance(getContext()).getDefaultAccount(); } vlog("state: primary " + mPrimaryAccount); // Parse the given raw contact deltas if (rawContactDeltas == null || rawContactDeltas.isEmpty()) { elog("No raw contact deltas"); if (mListener != null) mListener.onBindEditorsFailed(); return; } parseRawContactDeltas(rawContactDeltas); if (mKindSectionDataMap.isEmpty()) { elog("No kind section data parsed from RawContactDelta(s)"); if (mListener != null) mListener.onBindEditorsFailed(); return; } // Get the primary name kind section data mPrimaryNameKindSectionData = mKindSectionDataMap.get(StructuredName.CONTENT_ITEM_TYPE) .getEntryToWrite(/* id =*/ -1, mPrimaryAccount, mIsUserProfile); if (mPrimaryNameKindSectionData != null) { // Ensure that a structured name and photo exists final RawContactDelta rawContactDelta = mPrimaryNameKindSectionData.first.getRawContactDelta(); RawContactModifier.ensureKindExists( rawContactDelta, rawContactDelta.getAccountType(mAccountTypeManager), StructuredName.CONTENT_ITEM_TYPE); RawContactModifier.ensureKindExists( rawContactDelta, rawContactDelta.getAccountType(mAccountTypeManager), Photo.CONTENT_ITEM_TYPE); } // Setup the view addAccountInfo(rawContactDeltas); addPhotoView(); addKindSectionViews(); if (mIsExpanded) showAllFields(); if (mListener != null) mListener.onEditorsBound(); } private void parseRawContactDeltas(RawContactDeltaList rawContactDeltas) { // Build the kind section data list map vlog("parse: " + rawContactDeltas.size() + " rawContactDelta(s)"); for (int j = 0; j < rawContactDeltas.size(); j++) { final RawContactDelta rawContactDelta = rawContactDeltas.get(j); vlog("parse: " + j + " rawContactDelta" + rawContactDelta); if (rawContactDelta == null || !rawContactDelta.isVisible()) continue; final AccountType accountType = rawContactDelta.getAccountType(mAccountTypeManager); if (accountType == null) continue; final List<DataKind> dataKinds = accountType.getSortedDataKinds(); final int dataKindSize = dataKinds == null ? 0 : dataKinds.size(); vlog("parse: " + dataKindSize + " dataKinds(s)"); for (int i = 0; i < dataKindSize; i++) { final DataKind dataKind = dataKinds.get(i); if (dataKind == null || !dataKind.editable) { vlog("parse: " + i + " " + dataKind.mimeType + " dropped read-only"); continue; } final String mimeType = dataKind.mimeType; // Skip psuedo mime types if (DataKind.PSEUDO_MIME_TYPE_DISPLAY_NAME.equals(mimeType) || DataKind.PSEUDO_MIME_TYPE_PHONETIC_NAME.equals(mimeType)) { vlog("parse: " + i + " " + dataKind.mimeType + " dropped pseudo type"); continue; } final KindSectionDataList kindSectionDataList = getOrCreateKindSectionDataList(mimeType); final KindSectionData kindSectionData = new KindSectionData(accountType, dataKind, rawContactDelta); kindSectionDataList.add(kindSectionData); vlog("parse: " + i + " " + dataKind.mimeType + " " + kindSectionData.getValuesDeltas().size() + " value(s) " + kindSectionData.getNonEmptyValuesDeltas().size() + " non-empty value(s) " + kindSectionData.getVisibleValuesDeltas().size() + " visible value(s)"); } } } private KindSectionDataList getOrCreateKindSectionDataList(String mimeType) { KindSectionDataList kindSectionDataList = mKindSectionDataMap.get(mimeType); if (kindSectionDataList == null) { kindSectionDataList = new KindSectionDataList(); mKindSectionDataMap.put(mimeType, kindSectionDataList); } return kindSectionDataList; } private void addAccountInfo(RawContactDeltaList rawContactDeltas) { mAccountHeaderContainer.setVisibility(View.GONE); mAccountSelectorContainer.setVisibility(View.GONE); mRawContactContainer.setVisibility(View.GONE); if (mPrimaryNameKindSectionData == null) return; final RawContactDelta rawContactDelta = mPrimaryNameKindSectionData.first.getRawContactDelta(); // Get the account information for the primary raw contact delta final Pair<String,String> accountInfo = mIsUserProfile ? EditorUiUtils.getLocalAccountInfo(getContext(), rawContactDelta.getAccountName(), rawContactDelta.getAccountType(mAccountTypeManager)) : EditorUiUtils.getAccountInfo(getContext(), rawContactDelta.getAccountName(), rawContactDelta.getAccountType(mAccountTypeManager)); // Either the account header or selector should be shown, not both. final List<AccountWithDataSet> accounts = AccountTypeManager.getInstance(getContext()).getAccounts(true); if (mHasNewContact && !mIsUserProfile) { if (accounts.size() > 1) { addAccountSelector(accountInfo, rawContactDelta); } else { addAccountHeader(accountInfo); } } else if (mIsUserProfile || !shouldHideAccountContainer(rawContactDeltas)) { addAccountHeader(accountInfo); } // The raw contact selector should only display linked raw contacts that can be edited in // the full editor (i.e. they are not newly created raw contacts) final RawContactAccountListAdapter adapter = new RawContactAccountListAdapter(getContext(), getRawContactDeltaListForSelector(rawContactDeltas)); if (adapter.getCount() > 0) { final String accountsSummary = getResources().getQuantityString( R.plurals.compact_editor_linked_contacts_selector_title, adapter.getCount(), adapter.getCount()); addRawContactAccountSelector(accountsSummary, adapter); } } private RawContactDeltaList getRawContactDeltaListForSelector( RawContactDeltaList rawContactDeltas) { // Sort raw contacts so google accounts come first Collections.sort(rawContactDeltas, new RawContactDeltaComparator(getContext())); final RawContactDeltaList result = new RawContactDeltaList(); for (RawContactDelta rawContactDelta : rawContactDeltas) { if (rawContactDelta.isVisible() && rawContactDelta.getRawContactId() > 0) { // Only add raw contacts that can be opened in the editor result.add(rawContactDelta); } } // Don't return a list of size 1 that would just open the raw contact being edited // in the compact editor in the full editor if (result.size() == 1 && result.get(0).getRawContactAccountType( getContext()).areContactsWritable()) { result.clear(); return result; } return result; } // Returns true if there are multiple writable rawcontacts and no read-only ones, // or there are both writable and read-only rawcontacts. private boolean shouldHideAccountContainer(RawContactDeltaList rawContactDeltas) { int writable = 0; int readonly = 0; for (RawContactDelta rawContactDelta : rawContactDeltas) { if (rawContactDelta.isVisible() && rawContactDelta.getRawContactId() > 0) { if (rawContactDelta.getRawContactAccountType(getContext()).areContactsWritable()) { writable++; } else { readonly++; } } } return (writable > 1 || (writable > 0 && readonly > 0)); } private void addAccountHeader(Pair<String,String> accountInfo) { mAccountHeaderContainer.setVisibility(View.VISIBLE); // Set the account name final String accountName = TextUtils.isEmpty(accountInfo.first) ? accountInfo.second : accountInfo.first; mAccountHeaderName.setVisibility(View.VISIBLE); mAccountHeaderName.setText(accountName); // Set the account type final String selectorTitle = getResources().getString( R.string.compact_editor_account_selector_title); mAccountHeaderType.setText(selectorTitle); // Set the icon if (mPrimaryNameKindSectionData != null) { final RawContactDelta rawContactDelta = mPrimaryNameKindSectionData.first.getRawContactDelta(); if (rawContactDelta != null) { final AccountType accountType = rawContactDelta.getRawContactAccountType(getContext()); mAccountHeaderIcon.setImageDrawable(accountType.getDisplayIcon(getContext())); } } // Set the content description mAccountHeaderContainer.setContentDescription( EditorUiUtils.getAccountInfoContentDescription(accountName, selectorTitle)); } private void addAccountSelector(Pair<String,String> accountInfo, final RawContactDelta rawContactDelta) { mAccountSelectorContainer.setVisibility(View.VISIBLE); if (TextUtils.isEmpty(accountInfo.first)) { // Hide this view so the other text view will be centered vertically mAccountSelectorName.setVisibility(View.GONE); } else { mAccountSelectorName.setVisibility(View.VISIBLE); mAccountSelectorName.setText(accountInfo.first); } final String selectorTitle = getResources().getString( R.string.compact_editor_account_selector_title); mAccountSelectorType.setText(selectorTitle); mAccountSelectorContainer.setContentDescription(getResources().getString( R.string.compact_editor_account_selector_description, accountInfo.first)); mAccountSelectorContainer.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { final ListPopupWindow popup = new ListPopupWindow(getContext(), null); final AccountsListAdapter adapter = new AccountsListAdapter(getContext(), AccountsListAdapter.AccountListFilter.ACCOUNTS_CONTACT_WRITABLE, mPrimaryAccount); popup.setWidth(mAccountSelectorContainer.getWidth()); popup.setAnchorView(mAccountSelectorContainer); popup.setAdapter(adapter); popup.setModal(true); popup.setInputMethodMode(ListPopupWindow.INPUT_METHOD_NOT_NEEDED); popup.setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { UiClosables.closeQuietly(popup); final AccountWithDataSet newAccount = adapter.getItem(position); if (mListener != null && !mPrimaryAccount.equals(newAccount)) { mListener.onRebindEditorsForNewContact( rawContactDelta, mPrimaryAccount, newAccount); } } }); popup.show(); } }); } private void addRawContactAccountSelector(String accountsSummary, final RawContactAccountListAdapter adapter) { mRawContactContainer.setVisibility(View.VISIBLE); mRawContactSummary.setText(accountsSummary); mRawContactContainer.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { final ListPopupWindow popup = new ListPopupWindow(getContext(), null); popup.setWidth(mRawContactContainer.getWidth()); popup.setAnchorView(mRawContactContainer); popup.setAdapter(adapter); popup.setModal(true); popup.setInputMethodMode(ListPopupWindow.INPUT_METHOD_NOT_NEEDED); popup.setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { UiClosables.closeQuietly(popup); if (mListener != null) { final long rawContactId = adapter.getItemId(position); final Uri rawContactUri = ContentUris.withAppendedId( ContactsContract.RawContacts.CONTENT_URI, rawContactId); final RawContactDelta rawContactDelta = adapter.getItem(position); final AccountTypeManager accountTypes = AccountTypeManager.getInstance( getContext()); final AccountType accountType = rawContactDelta.getAccountType( accountTypes); final boolean isReadOnly = !accountType.areContactsWritable(); mListener.onRawContactSelected(rawContactUri, rawContactId, isReadOnly); } } }); popup.show(); } }); } private void addPhotoView() { // Get the kind section data and values delta that we will display in the photo view final KindSectionDataList kindSectionDataList = mKindSectionDataMap.get(Photo.CONTENT_ITEM_TYPE); final Pair<KindSectionData,ValuesDelta> photoToDisplay = kindSectionDataList.getEntryToDisplay(mPhotoId); if (photoToDisplay == null) { wlog("photo: no kind section data parsed"); mPhotoView.setVisibility(View.GONE); return; } // Set the photo view mPhotoView.setPhoto(photoToDisplay.second, mMaterialPalette); // Find the raw contact ID and values delta that will be written when the photo is edited final Pair<KindSectionData, ValuesDelta> photoToWrite = kindSectionDataList.getEntryToWrite( mPhotoId, mPrimaryAccount, mIsUserProfile); if (photoToWrite == null) { mPhotoView.setReadOnly(true); return; } mPhotoView.setReadOnly(false); mPhotoRawContactId = photoToWrite.first.getRawContactDelta().getRawContactId(); mPhotoValuesDelta = photoToWrite.second; } private void addKindSectionViews() { // Sort the kinds final TreeSet<Map.Entry<String,KindSectionDataList>> entries = new TreeSet<>(KIND_SECTION_DATA_MAP_ENTRY_COMPARATOR); entries.addAll(mKindSectionDataMap.entrySet()); vlog("kind: " + entries.size() + " kindSection(s)"); int i = -1; for (Map.Entry<String, KindSectionDataList> entry : entries) { i++; final String mimeType = entry.getKey(); if (StructuredName.CONTENT_ITEM_TYPE.equals(mimeType)) { if (mPrimaryNameKindSectionData == null) { vlog("kind: " + i + " " + mimeType + " dropped"); continue; } vlog("kind: " + i + " " + mimeType + " using first entry only"); final KindSectionDataList kindSectionDataList = new KindSectionDataList(); kindSectionDataList.add(mPrimaryNameKindSectionData.first); final CompactKindSectionView kindSectionView = inflateKindSectionView( mKindSectionViews, kindSectionDataList, mimeType, mPrimaryNameKindSectionData.second); mKindSectionViews.addView(kindSectionView); // Keep a pointer to all the KindSectionsViews for each mimeType getKindSectionViews(mimeType).add(kindSectionView); } else { final KindSectionDataList kindSectionDataList = entry.getValue(); // Ignore mime types that we've already handled if (Photo.CONTENT_ITEM_TYPE.equals(mimeType)) { vlog("kind: " + i + " " + mimeType + " dropped"); continue; } // Don't show more than one group editor on the compact editor. // Groups will still be editable for each raw contact individually on the full editor. if (GroupMembership.CONTENT_ITEM_TYPE.equals(mimeType) && kindSectionDataList.size() > 1) { vlog("kind: " + i + " " + mimeType + " dropped"); continue; } if (kindSectionDataList != null && !kindSectionDataList.isEmpty()) { vlog("kind: " + i + " " + mimeType + " " + kindSectionDataList.size() + " kindSectionData(s)"); final CompactKindSectionView kindSectionView = inflateKindSectionView( mKindSectionViews, kindSectionDataList, mimeType, /* primaryValueDelta =*/ null); mKindSectionViews.addView(kindSectionView); // Keep a pointer to all the KindSectionsViews for each mimeType getKindSectionViews(mimeType).add(kindSectionView); } } } } private List<CompactKindSectionView> getKindSectionViews(String mimeType) { List<CompactKindSectionView> kindSectionViews = mKindSectionViewsMap.get(mimeType); if (kindSectionViews == null) { kindSectionViews = new ArrayList<>(); mKindSectionViewsMap.put(mimeType, kindSectionViews); } return kindSectionViews; } private CompactKindSectionView inflateKindSectionView(ViewGroup viewGroup, KindSectionDataList kindSectionDataList, String mimeType, ValuesDelta primaryValuesDelta) { final CompactKindSectionView kindSectionView = (CompactKindSectionView) mLayoutInflater.inflate(R.layout.compact_item_kind_section, viewGroup, /* attachToRoot =*/ false); kindSectionView.setIsUserProfile(mIsUserProfile); if (Phone.CONTENT_ITEM_TYPE.equals(mimeType) || Email.CONTENT_ITEM_TYPE.equals(mimeType)) { // Phone numbers and email addresses are always displayed, // even if they are empty kindSectionView.setHideWhenEmpty(false); } // Since phone numbers and email addresses displayed even if they are empty, // they will be the only types you add new values to initially for new contacts kindSectionView.setShowOneEmptyEditor(true); // Sort non-name editors so they wind up in the order we want if (!StructuredName.CONTENT_ITEM_TYPE.equals(mimeType)) { Collections.sort(kindSectionDataList, new EditorComparator(getContext())); } kindSectionView.setState(kindSectionDataList, mViewIdGenerator, mListener, primaryValuesDelta); return kindSectionView; } void maybeSetReadOnlyDisplayNameAsPrimary(String readOnlyDisplayName) { if (TextUtils.isEmpty(readOnlyDisplayName)) return; final CompactKindSectionView primaryNameKindSectionView = getPrimaryNameKindSectionView(); if (primaryNameKindSectionView != null && primaryNameKindSectionView.isEmptyName()) { vlog("name: using read only display name as primary name"); primaryNameKindSectionView.setName(readOnlyDisplayName); } } private CompactKindSectionView getPrimaryNameKindSectionView() { final List<CompactKindSectionView> kindSectionViews = mKindSectionViewsMap.get(StructuredName.CONTENT_ITEM_TYPE); return kindSectionViews == null || kindSectionViews.isEmpty() ? null : kindSectionViews.get(0); } private void showAllFields() { // Stop hiding empty editors and allow the user to enter values for all kinds now for (int i = 0; i < mKindSectionViews.getChildCount(); i++) { final CompactKindSectionView kindSectionView = (CompactKindSectionView) mKindSectionViews.getChildAt(i); kindSectionView.setHideWhenEmpty(false); kindSectionView.updateEmptyEditors(/* shouldAnimate =*/ true); } mIsExpanded = true; // Hide the more fields button mMoreFields.setVisibility(View.GONE); } private static void vlog(String message) { if (Log.isLoggable(TAG, Log.VERBOSE)) { Log.v(TAG, message); } } private static void wlog(String message) { if (Log.isLoggable(TAG, Log.WARN)) { Log.w(TAG, message); } } private static void elog(String message) { Log.e(TAG, message); } }