Home | History | Annotate | Download | only in editor
      1 /*
      2  * Copyright (C) 2015 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.contacts.editor;
     18 
     19 import com.android.contacts.R;
     20 import com.android.contacts.common.model.AccountTypeManager;
     21 import com.android.contacts.common.model.RawContactDelta;
     22 import com.android.contacts.common.model.RawContactDeltaList;
     23 import com.android.contacts.common.model.RawContactModifier;
     24 import com.android.contacts.common.model.ValuesDelta;
     25 import com.android.contacts.common.model.account.AccountType;
     26 import com.android.contacts.common.model.account.AccountWithDataSet;
     27 import com.android.contacts.common.model.dataitem.DataKind;
     28 import com.android.contacts.common.util.AccountsListAdapter;
     29 import com.android.contacts.common.util.MaterialColorMapUtils;
     30 import com.android.contacts.util.UiClosables;
     31 
     32 import android.content.ContentUris;
     33 import android.content.Context;
     34 import android.database.Cursor;
     35 import android.graphics.Bitmap;
     36 import android.net.Uri;
     37 import android.os.Bundle;
     38 import android.os.Parcel;
     39 import android.os.Parcelable;
     40 import android.provider.ContactsContract;
     41 import android.provider.ContactsContract.CommonDataKinds.Email;
     42 import android.provider.ContactsContract.CommonDataKinds.Event;
     43 import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
     44 import android.provider.ContactsContract.CommonDataKinds.Im;
     45 import android.provider.ContactsContract.CommonDataKinds.Nickname;
     46 import android.provider.ContactsContract.CommonDataKinds.Note;
     47 import android.provider.ContactsContract.CommonDataKinds.Organization;
     48 import android.provider.ContactsContract.CommonDataKinds.Phone;
     49 import android.provider.ContactsContract.CommonDataKinds.Photo;
     50 import android.provider.ContactsContract.CommonDataKinds.Relation;
     51 import android.provider.ContactsContract.CommonDataKinds.SipAddress;
     52 import android.provider.ContactsContract.CommonDataKinds.StructuredName;
     53 import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
     54 import android.provider.ContactsContract.CommonDataKinds.Website;
     55 import android.text.TextUtils;
     56 import android.util.AttributeSet;
     57 import android.util.Log;
     58 import android.util.Pair;
     59 import android.view.LayoutInflater;
     60 import android.view.View;
     61 import android.view.ViewGroup;
     62 import android.widget.AdapterView;
     63 import android.widget.BaseAdapter;
     64 import android.widget.ImageView;
     65 import android.widget.LinearLayout;
     66 import android.widget.ListPopupWindow;
     67 import android.widget.TextView;
     68 
     69 import java.io.FileNotFoundException;
     70 import java.util.ArrayList;
     71 import java.util.Arrays;
     72 import java.util.Collections;
     73 import java.util.Comparator;
     74 import java.util.HashMap;
     75 import java.util.List;
     76 import java.util.Map;
     77 import java.util.TreeSet;
     78 
     79 /**
     80  * View to display information from multiple {@link RawContactDelta}s grouped together.
     81  */
     82 public class CompactRawContactsEditorView extends LinearLayout implements View.OnClickListener {
     83 
     84     static final String TAG = "CompactEditorView";
     85 
     86     private static final KindSectionDataMapEntryComparator
     87             KIND_SECTION_DATA_MAP_ENTRY_COMPARATOR = new KindSectionDataMapEntryComparator();
     88 
     89     /**
     90      * Callbacks for hosts of {@link CompactRawContactsEditorView}s.
     91      */
     92     public interface Listener {
     93 
     94         /**
     95          * Invoked when the structured name editor field has changed.
     96          *
     97          * @param rawContactId The raw contact ID from the underlying {@link RawContactDelta}.
     98          * @param valuesDelta The values from the underlying {@link RawContactDelta}.
     99          */
    100         public void onNameFieldChanged(long rawContactId, ValuesDelta valuesDelta);
    101 
    102         /**
    103          * Invoked when the compact editor should rebind editors for a new account.
    104          *
    105          * @param oldState Old data being edited.
    106          * @param oldAccount Old account associated with oldState.
    107          * @param newAccount New account to be used.
    108          */
    109         public void onRebindEditorsForNewContact(RawContactDelta oldState,
    110                 AccountWithDataSet oldAccount, AccountWithDataSet newAccount);
    111 
    112         /**
    113          * Invoked when no editors could be bound for the contact.
    114          */
    115         public void onBindEditorsFailed();
    116 
    117         /**
    118          * Invoked after editors have been bound for the contact.
    119          */
    120         public void onEditorsBound();
    121 
    122         /**
    123          * Invoked when a rawcontact from linked contacts is selected in editor.
    124          */
    125         public void onRawContactSelected(Uri uri, long rawContactId, boolean isReadOnly);
    126 
    127         /**
    128          * Returns the map of raw contact IDs to newly taken or selected photos that have not
    129          * yet been saved to CP2.
    130          */
    131         public Bundle getUpdatedPhotos();
    132     }
    133 
    134     /**
    135      * Used to list the account info for the given raw contacts list.
    136      */
    137     private static final class RawContactAccountListAdapter extends BaseAdapter {
    138         private final LayoutInflater mInflater;
    139         private final Context mContext;
    140         private final RawContactDeltaList mRawContactDeltas;
    141 
    142         public RawContactAccountListAdapter(Context context, RawContactDeltaList rawContactDeltas) {
    143             mContext = context;
    144             mRawContactDeltas = new RawContactDeltaList();
    145             for (RawContactDelta rawContactDelta : rawContactDeltas) {
    146                 if (rawContactDelta.isVisible() && rawContactDelta.getRawContactId() > 0) {
    147                     mRawContactDeltas.add(rawContactDelta);
    148                 }
    149             }
    150             mInflater = LayoutInflater.from(context);
    151         }
    152 
    153         @Override
    154         public View getView(int position, View convertView, ViewGroup parent) {
    155             final View resultView = convertView != null ? convertView
    156                     : mInflater.inflate(R.layout.account_selector_list_item, parent, false);
    157 
    158             final RawContactDelta rawContactDelta = mRawContactDeltas.get(position);
    159 
    160             final TextView text1 = (TextView) resultView.findViewById(android.R.id.text1);
    161             final AccountType accountType = rawContactDelta.getRawContactAccountType(mContext);
    162             text1.setText(accountType.getDisplayLabel(mContext));
    163 
    164             final TextView text2 = (TextView) resultView.findViewById(android.R.id.text2);
    165             final String accountName = rawContactDelta.getAccountName();
    166             if (TextUtils.isEmpty(accountName)) {
    167                 text2.setVisibility(View.GONE);
    168             } else {
    169                 // Truncate email addresses in the middle so we don't lose the domain
    170                 text2.setText(accountName);
    171                 text2.setEllipsize(TextUtils.TruncateAt.MIDDLE);
    172             }
    173 
    174             final ImageView icon = (ImageView) resultView.findViewById(android.R.id.icon);
    175             icon.setImageDrawable(accountType.getDisplayIcon(mContext));
    176 
    177             return resultView;
    178         }
    179 
    180         @Override
    181         public int getCount() {
    182             return mRawContactDeltas.size();
    183         }
    184 
    185         @Override
    186         public RawContactDelta getItem(int position) {
    187             return mRawContactDeltas.get(position);
    188         }
    189 
    190         @Override
    191         public long getItemId(int position) {
    192             return getItem(position).getRawContactId();
    193         }
    194     }
    195 
    196     /** Used to sort entire kind sections. */
    197     private static final class KindSectionDataMapEntryComparator implements
    198             Comparator<Map.Entry<String,KindSectionDataList>> {
    199 
    200         final MimeTypeComparator mMimeTypeComparator = new MimeTypeComparator();
    201 
    202         @Override
    203         public int compare(Map.Entry<String, KindSectionDataList> entry1,
    204                 Map.Entry<String, KindSectionDataList> entry2) {
    205             if (entry1 == entry2) return 0;
    206             if (entry1 == null) return -1;
    207             if (entry2 == null) return 1;
    208 
    209             final String mimeType1 = entry1.getKey();
    210             final String mimeType2 = entry2.getKey();
    211 
    212             return mMimeTypeComparator.compare(mimeType1, mimeType2);
    213         }
    214     }
    215 
    216     /**
    217      * Sorts kinds roughly the same as quick contacts; we diverge in the following ways:
    218      * <ol>
    219      *     <li>All names are together at the top.</li>
    220      *     <li>IM is moved up after addresses</li>
    221      *     <li>SIP addresses are moved to below phone numbers</li>
    222      *     <li>Group membership is placed at the end</li>
    223      * </ol>
    224      */
    225     private static final class MimeTypeComparator implements Comparator<String> {
    226 
    227         private static final List<String> MIME_TYPE_ORDER = Arrays.asList(new String[] {
    228                 StructuredName.CONTENT_ITEM_TYPE,
    229                 Nickname.CONTENT_ITEM_TYPE,
    230                 Organization.CONTENT_ITEM_TYPE,
    231                 Phone.CONTENT_ITEM_TYPE,
    232                 SipAddress.CONTENT_ITEM_TYPE,
    233                 Email.CONTENT_ITEM_TYPE,
    234                 StructuredPostal.CONTENT_ITEM_TYPE,
    235                 Im.CONTENT_ITEM_TYPE,
    236                 Website.CONTENT_ITEM_TYPE,
    237                 Event.CONTENT_ITEM_TYPE,
    238                 Relation.CONTENT_ITEM_TYPE,
    239                 Note.CONTENT_ITEM_TYPE,
    240                 GroupMembership.CONTENT_ITEM_TYPE
    241         });
    242 
    243         @Override
    244         public int compare(String mimeType1, String mimeType2) {
    245             if (mimeType1 == mimeType2) return 0;
    246             if (mimeType1 == null) return -1;
    247             if (mimeType2 == null) return 1;
    248 
    249             int index1 = MIME_TYPE_ORDER.indexOf(mimeType1);
    250             int index2 = MIME_TYPE_ORDER.indexOf(mimeType2);
    251 
    252             // Fallback to alphabetical ordering of the mime type if both are not found
    253             if (index1 < 0 && index2 < 0) return mimeType1.compareTo(mimeType2);
    254             if (index1 < 0) return 1;
    255             if (index2 < 0) return -1;
    256 
    257             return index1 < index2 ? -1 : 1;
    258         }
    259     }
    260 
    261     /**
    262      * Sorts primary accounts and google account types before others.
    263      */
    264     private static final class EditorComparator implements Comparator<KindSectionData> {
    265 
    266         private RawContactDeltaComparator mRawContactDeltaComparator;
    267 
    268         private EditorComparator(Context context) {
    269             mRawContactDeltaComparator = new RawContactDeltaComparator(context);
    270         }
    271 
    272         @Override
    273         public int compare(KindSectionData kindSectionData1, KindSectionData kindSectionData2) {
    274             if (kindSectionData1 == kindSectionData2) return 0;
    275             if (kindSectionData1 == null) return -1;
    276             if (kindSectionData2 == null) return 1;
    277 
    278             final RawContactDelta rawContactDelta1 = kindSectionData1.getRawContactDelta();
    279             final RawContactDelta rawContactDelta2 = kindSectionData2.getRawContactDelta();
    280 
    281             if (rawContactDelta1 == rawContactDelta2) return 0;
    282             if (rawContactDelta1 == null) return -1;
    283             if (rawContactDelta2 == null) return 1;
    284 
    285             return mRawContactDeltaComparator.compare(rawContactDelta1, rawContactDelta2);
    286         }
    287     }
    288 
    289     public static class SavedState extends BaseSavedState {
    290 
    291         public static final Parcelable.Creator<SavedState> CREATOR =
    292                 new Parcelable.Creator<SavedState>() {
    293                     public SavedState createFromParcel(Parcel in) {
    294                         return new SavedState(in);
    295                     }
    296                     public SavedState[] newArray(int size) {
    297                         return new SavedState[size];
    298                     }
    299                 };
    300 
    301         private boolean mIsExpanded;
    302 
    303         public SavedState(Parcelable superState) {
    304             super(superState);
    305         }
    306 
    307         private SavedState(Parcel in) {
    308             super(in);
    309             mIsExpanded = in.readInt() != 0;
    310         }
    311 
    312         @Override
    313         public void writeToParcel(Parcel out, int flags) {
    314             super.writeToParcel(out, flags);
    315             out.writeInt(mIsExpanded ? 1 : 0);
    316         }
    317     }
    318 
    319     private CompactRawContactsEditorView.Listener mListener;
    320 
    321     private AccountTypeManager mAccountTypeManager;
    322     private LayoutInflater mLayoutInflater;
    323 
    324     private ViewIdGenerator mViewIdGenerator;
    325     private MaterialColorMapUtils.MaterialPalette mMaterialPalette;
    326     private long mPhotoId = -1;
    327     private boolean mHasNewContact;
    328     private boolean mIsUserProfile;
    329     private AccountWithDataSet mPrimaryAccount;
    330     private Map<String,KindSectionDataList> mKindSectionDataMap = new HashMap<>();
    331 
    332     // Account header
    333     private View mAccountHeaderContainer;
    334     private TextView mAccountHeaderType;
    335     private TextView mAccountHeaderName;
    336     private ImageView mAccountHeaderIcon;
    337 
    338     // Account selector
    339     private View mAccountSelectorContainer;
    340     private View mAccountSelector;
    341     private TextView mAccountSelectorType;
    342     private TextView mAccountSelectorName;
    343 
    344     // Raw contacts selector
    345     private View mRawContactContainer;
    346     private TextView mRawContactSummary;
    347 
    348     private CompactPhotoEditorView mPhotoView;
    349     private ViewGroup mKindSectionViews;
    350     private Map<String,List<CompactKindSectionView>> mKindSectionViewsMap = new HashMap<>();
    351     private View mMoreFields;
    352 
    353     private boolean mIsExpanded;
    354 
    355     private long mPhotoRawContactId;
    356     private ValuesDelta mPhotoValuesDelta;
    357 
    358     private Pair<KindSectionData, ValuesDelta> mPrimaryNameKindSectionData;
    359 
    360     public CompactRawContactsEditorView(Context context) {
    361         super(context);
    362     }
    363 
    364     public CompactRawContactsEditorView(Context context, AttributeSet attrs) {
    365         super(context, attrs);
    366     }
    367 
    368     /**
    369      * Sets the receiver for {@link CompactRawContactsEditorView} callbacks.
    370      */
    371     public void setListener(Listener listener) {
    372         mListener = listener;
    373     }
    374 
    375     @Override
    376     protected void onFinishInflate() {
    377         super.onFinishInflate();
    378 
    379         mAccountTypeManager = AccountTypeManager.getInstance(getContext());
    380         mLayoutInflater = (LayoutInflater)
    381                 getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    382 
    383         // Account header
    384         mAccountHeaderContainer = findViewById(R.id.account_container);
    385         mAccountHeaderType = (TextView) findViewById(R.id.account_type);
    386         mAccountHeaderName = (TextView) findViewById(R.id.account_name);
    387         mAccountHeaderIcon = (ImageView) findViewById(R.id.account_type_icon);
    388 
    389         // Account selector
    390         mAccountSelectorContainer = findViewById(R.id.account_selector_container);
    391         mAccountSelector = findViewById(R.id.account);
    392         mAccountSelectorType = (TextView) findViewById(R.id.account_type_selector);
    393         mAccountSelectorName = (TextView) findViewById(R.id.account_name_selector);
    394 
    395         // Raw contacts selector
    396         mRawContactContainer = findViewById(R.id.all_rawcontacts_accounts_container);
    397         mRawContactSummary = (TextView) findViewById(R.id.rawcontacts_accounts_summary);
    398 
    399         mPhotoView = (CompactPhotoEditorView) findViewById(R.id.photo_editor);
    400         mKindSectionViews = (LinearLayout) findViewById(R.id.kind_section_views);
    401         mMoreFields = findViewById(R.id.more_fields);
    402         mMoreFields.setOnClickListener(this);
    403     }
    404 
    405     @Override
    406     public void onClick(View view) {
    407         if (view.getId() == R.id.more_fields) {
    408             showAllFields();
    409         }
    410     }
    411 
    412     @Override
    413     public void setEnabled(boolean enabled) {
    414         super.setEnabled(enabled);
    415         final int childCount = mKindSectionViews.getChildCount();
    416         for (int i = 0; i < childCount; i++) {
    417             mKindSectionViews.getChildAt(i).setEnabled(enabled);
    418         }
    419     }
    420 
    421     @Override
    422     public Parcelable onSaveInstanceState() {
    423         final Parcelable superState = super.onSaveInstanceState();
    424         final SavedState savedState = new SavedState(superState);
    425         savedState.mIsExpanded = mIsExpanded;
    426         return savedState;
    427     }
    428 
    429     @Override
    430     public void onRestoreInstanceState(Parcelable state) {
    431         if(!(state instanceof SavedState)) {
    432             super.onRestoreInstanceState(state);
    433             return;
    434         }
    435         final SavedState savedState = (SavedState) state;
    436         super.onRestoreInstanceState(savedState.getSuperState());
    437         mIsExpanded = savedState.mIsExpanded;
    438         if (mIsExpanded) {
    439             showAllFields();
    440         }
    441     }
    442 
    443     /**
    444      * Pass through to {@link CompactPhotoEditorView#setListener}.
    445      */
    446     public void setPhotoListener(CompactPhotoEditorView.Listener listener) {
    447         mPhotoView.setListener(listener);
    448     }
    449 
    450     public void removePhoto() {
    451         mPhotoValuesDelta.setFromTemplate(true);
    452         mPhotoValuesDelta.put(Photo.PHOTO, (byte[]) null);
    453 
    454         mPhotoView.removePhoto();
    455     }
    456 
    457     /**
    458      * Pass through to {@link CompactPhotoEditorView#setFullSizedPhoto(Uri)}.
    459      */
    460     public void setFullSizePhoto(Uri photoUri) {
    461         mPhotoView.setFullSizedPhoto(photoUri);
    462     }
    463 
    464     public void updatePhoto(Uri photoUri) {
    465         mPhotoValuesDelta.setFromTemplate(false);
    466         // Unset primary for all photos
    467         unsetSuperPrimaryFromAllPhotos();
    468         // Mark the currently displayed photo as primary
    469         mPhotoValuesDelta.setSuperPrimary(true);
    470 
    471         // Even though high-res photos cannot be saved by passing them via
    472         // an EntityDeltaList (since they cause the Bundle size limit to be
    473         // exceeded), we still pass a low-res thumbnail. This simplifies
    474         // code all over the place, because we don't have to test whether
    475         // there is a change in EITHER the delta-list OR a changed photo...
    476         // this way, there is always a change in the delta-list.
    477         try {
    478             final byte[] bytes = EditorUiUtils.getCompressedThumbnailBitmapBytes(
    479                     getContext(), photoUri);
    480             if (bytes != null) {
    481                 mPhotoValuesDelta.setPhoto(bytes);
    482             }
    483         } catch (FileNotFoundException e) {
    484             elog("Failed to get bitmap from photo Uri");
    485         }
    486 
    487         mPhotoView.setFullSizedPhoto(photoUri);
    488     }
    489 
    490     private void unsetSuperPrimaryFromAllPhotos() {
    491         final List<KindSectionData> kindSectionDataList =
    492                 mKindSectionDataMap.get(Photo.CONTENT_ITEM_TYPE);
    493         for (KindSectionData kindSectionData : kindSectionDataList) {
    494             for (ValuesDelta valuesDelta : kindSectionData.getNonEmptyValuesDeltas()) {
    495                 valuesDelta.setSuperPrimary(false);
    496             }
    497         }
    498     }
    499 
    500     /**
    501      * Pass through to {@link CompactPhotoEditorView#isWritablePhotoSet}.
    502      */
    503     public boolean isWritablePhotoSet() {
    504         return mPhotoView.isWritablePhotoSet();
    505     }
    506 
    507     /**
    508      * Get the raw contact ID for the CompactHeaderView photo.
    509      */
    510     public long getPhotoRawContactId() {
    511         return mPhotoRawContactId;
    512     }
    513 
    514     public StructuredNameEditorView getPrimaryNameEditorView() {
    515         final CompactKindSectionView primaryNameKindSectionView = getPrimaryNameKindSectionView();
    516         return primaryNameKindSectionView == null
    517                 ? null : primaryNameKindSectionView.getPrimaryNameEditorView();
    518     }
    519 
    520     /**
    521      * Returns a data holder for every non-default/non-empty photo from each raw contact, whether
    522      * the raw contact is writable or not.
    523      */
    524     public ArrayList<CompactPhotoSelectionFragment.Photo> getPhotos() {
    525         final ArrayList<CompactPhotoSelectionFragment.Photo> photos = new ArrayList<>();
    526 
    527         final Bundle updatedPhotos = mListener == null ? null : mListener.getUpdatedPhotos();
    528 
    529         final List<KindSectionData> kindSectionDataList =
    530                 mKindSectionDataMap.get(Photo.CONTENT_ITEM_TYPE);
    531         for (int i = 0; i < kindSectionDataList.size(); i++) {
    532             final KindSectionData kindSectionData = kindSectionDataList.get(i);
    533             final AccountType accountType = kindSectionData.getAccountType();
    534             final List<ValuesDelta> valuesDeltas = kindSectionData.getNonEmptyValuesDeltas();
    535             if (valuesDeltas.isEmpty()) continue;
    536             for (int j = 0; j < valuesDeltas.size(); j++) {
    537                 final ValuesDelta valuesDelta = valuesDeltas.get(j);
    538                 final Bitmap bitmap = EditorUiUtils.getPhotoBitmap(valuesDelta);
    539                 if (bitmap == null) continue;
    540 
    541                 final CompactPhotoSelectionFragment.Photo photo =
    542                         new CompactPhotoSelectionFragment.Photo();
    543                 photo.titleRes = accountType.titleRes;
    544                 photo.iconRes = accountType.iconRes;
    545                 photo.syncAdapterPackageName = accountType.syncAdapterPackageName;
    546                 photo.valuesDelta = valuesDelta;
    547                 photo.primary = valuesDelta.isSuperPrimary();
    548                 photo.kindSectionDataListIndex = i;
    549                 photo.valuesDeltaListIndex = j;
    550                 photo.photoId = valuesDelta.getId();
    551 
    552                 if (updatedPhotos != null) {
    553                     photo.updatedPhotoUri = (Uri) updatedPhotos.get(String.valueOf(
    554                             kindSectionData.getRawContactDelta().getRawContactId()));
    555                 }
    556 
    557                 final CharSequence accountTypeLabel = accountType.getDisplayLabel(getContext());
    558                 photo.accountType = accountTypeLabel == null ? "" : accountTypeLabel.toString();
    559                 final String accountName = kindSectionData.getRawContactDelta().getAccountName();
    560                 photo.accountName = accountName == null ? "" : accountName;
    561 
    562                 photos.add(photo);
    563             }
    564         }
    565 
    566         return photos;
    567     }
    568 
    569     /**
    570      * Marks the raw contact photo given as primary for the aggregate contact and updates the
    571      * UI.
    572      */
    573     public void setPrimaryPhoto(CompactPhotoSelectionFragment.Photo photo) {
    574         // Find the values delta to mark as primary
    575         final KindSectionDataList kindSectionDataList =
    576                 mKindSectionDataMap.get(Photo.CONTENT_ITEM_TYPE);
    577         if (photo.kindSectionDataListIndex < 0
    578                 || photo.kindSectionDataListIndex >= kindSectionDataList.size()) {
    579             wlog("Invalid kind section data list index");
    580             return;
    581         }
    582         final KindSectionData kindSectionData =
    583                 kindSectionDataList.get(photo.kindSectionDataListIndex);
    584         final List<ValuesDelta> valuesDeltaList = kindSectionData.getNonEmptyValuesDeltas();
    585         if (photo.valuesDeltaListIndex >= valuesDeltaList.size()) {
    586             wlog("Invalid values delta list index");
    587             return;
    588         }
    589 
    590         // Update values delta
    591         final ValuesDelta valuesDelta = valuesDeltaList.get(photo.valuesDeltaListIndex);
    592         valuesDelta.setFromTemplate(false);
    593         unsetSuperPrimaryFromAllPhotos();
    594         valuesDelta.setSuperPrimary(true);
    595 
    596         // Update the UI
    597         mPhotoView.setPhoto(valuesDelta, mMaterialPalette);
    598     }
    599 
    600     public View getAggregationAnchorView() {
    601         final List<CompactKindSectionView> kindSectionViews = getKindSectionViews(
    602                 StructuredName.CONTENT_ITEM_TYPE);
    603         if (!kindSectionViews.isEmpty()) {
    604             return mKindSectionViews.getChildAt(0).findViewById(R.id.anchor_view);
    605         }
    606         return null;
    607     }
    608 
    609     public void setGroupMetaData(Cursor groupMetaData) {
    610         final List<CompactKindSectionView> kindSectionViews = getKindSectionViews(
    611                 GroupMembership.CONTENT_ITEM_TYPE);
    612         for (CompactKindSectionView kindSectionView : kindSectionViews) {
    613             kindSectionView.setGroupMetaData(groupMetaData);
    614             if (mIsExpanded) {
    615                 kindSectionView.setHideWhenEmpty(false);
    616                 kindSectionView.updateEmptyEditors(/* shouldAnimate =*/ true);
    617             }
    618         }
    619     }
    620 
    621     public void setState(RawContactDeltaList rawContactDeltas,
    622             MaterialColorMapUtils.MaterialPalette materialPalette, ViewIdGenerator viewIdGenerator,
    623             long photoId, boolean hasNewContact, boolean isUserProfile,
    624             AccountWithDataSet primaryAccount) {
    625         mKindSectionDataMap.clear();
    626         mKindSectionViews.removeAllViews();
    627         mMoreFields.setVisibility(View.VISIBLE);
    628 
    629         mMaterialPalette = materialPalette;
    630         mViewIdGenerator = viewIdGenerator;
    631         mPhotoId = photoId;
    632 
    633         mHasNewContact = hasNewContact;
    634         mIsUserProfile = isUserProfile;
    635         mPrimaryAccount = primaryAccount;
    636         if (mPrimaryAccount == null) {
    637             mPrimaryAccount = ContactEditorUtils.getInstance(getContext()).getDefaultAccount();
    638         }
    639         vlog("state: primary " + mPrimaryAccount);
    640 
    641         // Parse the given raw contact deltas
    642         if (rawContactDeltas == null || rawContactDeltas.isEmpty()) {
    643             elog("No raw contact deltas");
    644             if (mListener != null) mListener.onBindEditorsFailed();
    645             return;
    646         }
    647         parseRawContactDeltas(rawContactDeltas);
    648         if (mKindSectionDataMap.isEmpty()) {
    649             elog("No kind section data parsed from RawContactDelta(s)");
    650             if (mListener != null) mListener.onBindEditorsFailed();
    651             return;
    652         }
    653 
    654         // Get the primary name kind section data
    655         mPrimaryNameKindSectionData = mKindSectionDataMap.get(StructuredName.CONTENT_ITEM_TYPE)
    656                 .getEntryToWrite(/* id =*/ -1, mPrimaryAccount, mIsUserProfile);
    657         if (mPrimaryNameKindSectionData != null) {
    658             // Ensure that a structured name and photo exists
    659             final RawContactDelta rawContactDelta =
    660                     mPrimaryNameKindSectionData.first.getRawContactDelta();
    661             RawContactModifier.ensureKindExists(
    662                     rawContactDelta,
    663                     rawContactDelta.getAccountType(mAccountTypeManager),
    664                     StructuredName.CONTENT_ITEM_TYPE);
    665             RawContactModifier.ensureKindExists(
    666                     rawContactDelta,
    667                     rawContactDelta.getAccountType(mAccountTypeManager),
    668                     Photo.CONTENT_ITEM_TYPE);
    669         }
    670 
    671         // Setup the view
    672         addAccountInfo(rawContactDeltas);
    673         addPhotoView();
    674         addKindSectionViews();
    675 
    676         if (mIsExpanded) showAllFields();
    677 
    678         if (mListener != null) mListener.onEditorsBound();
    679     }
    680 
    681     private void parseRawContactDeltas(RawContactDeltaList rawContactDeltas) {
    682         // Build the kind section data list map
    683         vlog("parse: " + rawContactDeltas.size() + " rawContactDelta(s)");
    684         for (int j = 0; j < rawContactDeltas.size(); j++) {
    685             final RawContactDelta rawContactDelta = rawContactDeltas.get(j);
    686             vlog("parse: " + j + " rawContactDelta" + rawContactDelta);
    687             if (rawContactDelta == null || !rawContactDelta.isVisible()) continue;
    688             final AccountType accountType = rawContactDelta.getAccountType(mAccountTypeManager);
    689             if (accountType == null) continue;
    690             final List<DataKind> dataKinds = accountType.getSortedDataKinds();
    691             final int dataKindSize = dataKinds == null ? 0 : dataKinds.size();
    692             vlog("parse: " + dataKindSize + " dataKinds(s)");
    693             for (int i = 0; i < dataKindSize; i++) {
    694                 final DataKind dataKind = dataKinds.get(i);
    695                 if (dataKind == null || !dataKind.editable) {
    696                     vlog("parse: " + i + " " + dataKind.mimeType + " dropped read-only");
    697                     continue;
    698                 }
    699                 final String mimeType = dataKind.mimeType;
    700 
    701                 // Skip psuedo mime types
    702                 if (DataKind.PSEUDO_MIME_TYPE_DISPLAY_NAME.equals(mimeType)
    703                         || DataKind.PSEUDO_MIME_TYPE_PHONETIC_NAME.equals(mimeType)) {
    704                     vlog("parse: " + i + " " + dataKind.mimeType + " dropped pseudo type");
    705                     continue;
    706                 }
    707 
    708                 final KindSectionDataList kindSectionDataList =
    709                         getOrCreateKindSectionDataList(mimeType);
    710                 final KindSectionData kindSectionData =
    711                         new KindSectionData(accountType, dataKind, rawContactDelta);
    712                 kindSectionDataList.add(kindSectionData);
    713 
    714                 vlog("parse: " + i + " " + dataKind.mimeType + " " +
    715                         kindSectionData.getValuesDeltas().size() + " value(s) " +
    716                         kindSectionData.getNonEmptyValuesDeltas().size() + " non-empty value(s) " +
    717                         kindSectionData.getVisibleValuesDeltas().size() +
    718                         " visible value(s)");
    719             }
    720         }
    721     }
    722 
    723     private KindSectionDataList getOrCreateKindSectionDataList(String mimeType) {
    724         KindSectionDataList kindSectionDataList = mKindSectionDataMap.get(mimeType);
    725         if (kindSectionDataList == null) {
    726             kindSectionDataList = new KindSectionDataList();
    727             mKindSectionDataMap.put(mimeType, kindSectionDataList);
    728         }
    729         return kindSectionDataList;
    730     }
    731 
    732     private void addAccountInfo(RawContactDeltaList rawContactDeltas) {
    733         mAccountHeaderContainer.setVisibility(View.GONE);
    734         mAccountSelectorContainer.setVisibility(View.GONE);
    735         mRawContactContainer.setVisibility(View.GONE);
    736 
    737         if (mPrimaryNameKindSectionData == null) return;
    738         final RawContactDelta rawContactDelta =
    739                 mPrimaryNameKindSectionData.first.getRawContactDelta();
    740 
    741         // Get the account information for the primary raw contact delta
    742         final Pair<String,String> accountInfo = mIsUserProfile
    743                 ? EditorUiUtils.getLocalAccountInfo(getContext(),
    744                         rawContactDelta.getAccountName(),
    745                         rawContactDelta.getAccountType(mAccountTypeManager))
    746                 : EditorUiUtils.getAccountInfo(getContext(),
    747                         rawContactDelta.getAccountName(),
    748                         rawContactDelta.getAccountType(mAccountTypeManager));
    749 
    750         // Either the account header or selector should be shown, not both.
    751         final List<AccountWithDataSet> accounts =
    752                 AccountTypeManager.getInstance(getContext()).getAccounts(true);
    753         if (mHasNewContact && !mIsUserProfile) {
    754             if (accounts.size() > 1) {
    755                 addAccountSelector(accountInfo, rawContactDelta);
    756             } else {
    757                 addAccountHeader(accountInfo);
    758             }
    759         } else if (mIsUserProfile || !shouldHideAccountContainer(rawContactDeltas)) {
    760             addAccountHeader(accountInfo);
    761         }
    762 
    763         // The raw contact selector should only display linked raw contacts that can be edited in
    764         // the full editor (i.e. they are not newly created raw contacts)
    765         final RawContactAccountListAdapter adapter =  new RawContactAccountListAdapter(getContext(),
    766                 getRawContactDeltaListForSelector(rawContactDeltas));
    767         if (adapter.getCount() > 0) {
    768             final String accountsSummary = getResources().getQuantityString(
    769                     R.plurals.compact_editor_linked_contacts_selector_title,
    770                     adapter.getCount(), adapter.getCount());
    771             addRawContactAccountSelector(accountsSummary, adapter);
    772         }
    773     }
    774 
    775     private RawContactDeltaList getRawContactDeltaListForSelector(
    776             RawContactDeltaList rawContactDeltas) {
    777         // Sort raw contacts so google accounts come first
    778         Collections.sort(rawContactDeltas, new RawContactDeltaComparator(getContext()));
    779 
    780         final RawContactDeltaList result = new RawContactDeltaList();
    781         for (RawContactDelta rawContactDelta : rawContactDeltas) {
    782             if (rawContactDelta.isVisible() && rawContactDelta.getRawContactId() > 0) {
    783                 // Only add raw contacts that can be opened in the editor
    784                 result.add(rawContactDelta);
    785             }
    786         }
    787         // Don't return a list of size 1 that would just open the raw contact being edited
    788         // in the compact editor in the full editor
    789         if (result.size() == 1 && result.get(0).getRawContactAccountType(
    790                 getContext()).areContactsWritable()) {
    791             result.clear();
    792             return result;
    793         }
    794         return result;
    795     }
    796 
    797     // Returns true if there are multiple writable rawcontacts and no read-only ones,
    798     // or there are both writable and read-only rawcontacts.
    799     private boolean shouldHideAccountContainer(RawContactDeltaList rawContactDeltas) {
    800         int writable = 0;
    801         int readonly = 0;
    802         for (RawContactDelta rawContactDelta : rawContactDeltas) {
    803             if (rawContactDelta.isVisible() && rawContactDelta.getRawContactId() > 0) {
    804                 if (rawContactDelta.getRawContactAccountType(getContext()).areContactsWritable()) {
    805                     writable++;
    806                 } else {
    807                     readonly++;
    808                 }
    809             }
    810         }
    811         return (writable > 1 || (writable > 0 && readonly > 0));
    812     }
    813 
    814     private void addAccountHeader(Pair<String,String> accountInfo) {
    815         mAccountHeaderContainer.setVisibility(View.VISIBLE);
    816 
    817         // Set the account name
    818         final String accountName = TextUtils.isEmpty(accountInfo.first)
    819                 ? accountInfo.second : accountInfo.first;
    820         mAccountHeaderName.setVisibility(View.VISIBLE);
    821         mAccountHeaderName.setText(accountName);
    822 
    823         // Set the account type
    824         final String selectorTitle = getResources().getString(
    825                 R.string.compact_editor_account_selector_title);
    826         mAccountHeaderType.setText(selectorTitle);
    827 
    828         // Set the icon
    829         if (mPrimaryNameKindSectionData != null) {
    830             final RawContactDelta rawContactDelta =
    831                     mPrimaryNameKindSectionData.first.getRawContactDelta();
    832             if (rawContactDelta != null) {
    833                 final AccountType accountType =
    834                         rawContactDelta.getRawContactAccountType(getContext());
    835                 mAccountHeaderIcon.setImageDrawable(accountType.getDisplayIcon(getContext()));
    836             }
    837         }
    838 
    839         // Set the content description
    840         mAccountHeaderContainer.setContentDescription(
    841                 EditorUiUtils.getAccountInfoContentDescription(accountName, selectorTitle));
    842     }
    843 
    844     private void addAccountSelector(Pair<String,String> accountInfo,
    845             final RawContactDelta rawContactDelta) {
    846         mAccountSelectorContainer.setVisibility(View.VISIBLE);
    847 
    848         if (TextUtils.isEmpty(accountInfo.first)) {
    849             // Hide this view so the other text view will be centered vertically
    850             mAccountSelectorName.setVisibility(View.GONE);
    851         } else {
    852             mAccountSelectorName.setVisibility(View.VISIBLE);
    853             mAccountSelectorName.setText(accountInfo.first);
    854         }
    855 
    856         final String selectorTitle = getResources().getString(
    857                 R.string.compact_editor_account_selector_title);
    858         mAccountSelectorType.setText(selectorTitle);
    859 
    860         mAccountSelectorContainer.setContentDescription(getResources().getString(
    861                 R.string.compact_editor_account_selector_description, accountInfo.first));
    862 
    863         mAccountSelectorContainer.setOnClickListener(new View.OnClickListener() {
    864             @Override
    865             public void onClick(View v) {
    866                 final ListPopupWindow popup = new ListPopupWindow(getContext(), null);
    867                 final AccountsListAdapter adapter =
    868                         new AccountsListAdapter(getContext(),
    869                                 AccountsListAdapter.AccountListFilter.ACCOUNTS_CONTACT_WRITABLE,
    870                                 mPrimaryAccount);
    871                 popup.setWidth(mAccountSelectorContainer.getWidth());
    872                 popup.setAnchorView(mAccountSelectorContainer);
    873                 popup.setAdapter(adapter);
    874                 popup.setModal(true);
    875                 popup.setInputMethodMode(ListPopupWindow.INPUT_METHOD_NOT_NEEDED);
    876                 popup.setOnItemClickListener(new AdapterView.OnItemClickListener() {
    877                     @Override
    878                     public void onItemClick(AdapterView<?> parent, View view, int position,
    879                             long id) {
    880                         UiClosables.closeQuietly(popup);
    881                         final AccountWithDataSet newAccount = adapter.getItem(position);
    882                         if (mListener != null && !mPrimaryAccount.equals(newAccount)) {
    883                             mListener.onRebindEditorsForNewContact(
    884                                     rawContactDelta,
    885                                     mPrimaryAccount,
    886                                     newAccount);
    887                         }
    888                     }
    889                 });
    890                 popup.show();
    891             }
    892         });
    893     }
    894 
    895     private void addRawContactAccountSelector(String accountsSummary,
    896             final RawContactAccountListAdapter adapter) {
    897         mRawContactContainer.setVisibility(View.VISIBLE);
    898 
    899         mRawContactSummary.setText(accountsSummary);
    900 
    901         mRawContactContainer.setOnClickListener(new View.OnClickListener() {
    902             @Override
    903             public void onClick(View v) {
    904                 final ListPopupWindow popup = new ListPopupWindow(getContext(), null);
    905                 popup.setWidth(mRawContactContainer.getWidth());
    906                 popup.setAnchorView(mRawContactContainer);
    907                 popup.setAdapter(adapter);
    908                 popup.setModal(true);
    909                 popup.setInputMethodMode(ListPopupWindow.INPUT_METHOD_NOT_NEEDED);
    910                 popup.setOnItemClickListener(new AdapterView.OnItemClickListener() {
    911                     @Override
    912                     public void onItemClick(AdapterView<?> parent, View view, int position,
    913                                             long id) {
    914                         UiClosables.closeQuietly(popup);
    915 
    916                         if (mListener != null) {
    917                             final long rawContactId = adapter.getItemId(position);
    918                             final Uri rawContactUri = ContentUris.withAppendedId(
    919                                     ContactsContract.RawContacts.CONTENT_URI, rawContactId);
    920                             final RawContactDelta rawContactDelta = adapter.getItem(position);
    921                             final AccountTypeManager accountTypes = AccountTypeManager.getInstance(
    922                                     getContext());
    923                             final AccountType accountType = rawContactDelta.getAccountType(
    924                                     accountTypes);
    925                             final boolean isReadOnly = !accountType.areContactsWritable();
    926 
    927                             mListener.onRawContactSelected(rawContactUri, rawContactId, isReadOnly);
    928                         }
    929                     }
    930                 });
    931                 popup.show();
    932             }
    933         });
    934     }
    935 
    936     private void addPhotoView() {
    937         // Get the kind section data and values delta that we will display in the photo view
    938         final KindSectionDataList kindSectionDataList =
    939                 mKindSectionDataMap.get(Photo.CONTENT_ITEM_TYPE);
    940         final Pair<KindSectionData,ValuesDelta> photoToDisplay =
    941                 kindSectionDataList.getEntryToDisplay(mPhotoId);
    942         if (photoToDisplay == null) {
    943             wlog("photo: no kind section data parsed");
    944             mPhotoView.setVisibility(View.GONE);
    945             return;
    946         }
    947 
    948         // Set the photo view
    949         mPhotoView.setPhoto(photoToDisplay.second, mMaterialPalette);
    950 
    951         // Find the raw contact ID and values delta that will be written when the photo is edited
    952         final Pair<KindSectionData, ValuesDelta> photoToWrite = kindSectionDataList.getEntryToWrite(
    953                 mPhotoId, mPrimaryAccount, mIsUserProfile);
    954         if (photoToWrite == null) {
    955             mPhotoView.setReadOnly(true);
    956             return;
    957         }
    958         mPhotoView.setReadOnly(false);
    959         mPhotoRawContactId = photoToWrite.first.getRawContactDelta().getRawContactId();
    960         mPhotoValuesDelta = photoToWrite.second;
    961     }
    962 
    963     private void addKindSectionViews() {
    964         // Sort the kinds
    965         final TreeSet<Map.Entry<String,KindSectionDataList>> entries =
    966                 new TreeSet<>(KIND_SECTION_DATA_MAP_ENTRY_COMPARATOR);
    967         entries.addAll(mKindSectionDataMap.entrySet());
    968 
    969         vlog("kind: " + entries.size() + " kindSection(s)");
    970         int i = -1;
    971         for (Map.Entry<String, KindSectionDataList> entry : entries) {
    972             i++;
    973 
    974             final String mimeType = entry.getKey();
    975 
    976             if (StructuredName.CONTENT_ITEM_TYPE.equals(mimeType)) {
    977                 if (mPrimaryNameKindSectionData == null) {
    978                     vlog("kind: " + i + " " + mimeType + " dropped");
    979                     continue;
    980                 }
    981                 vlog("kind: " + i + " " + mimeType + " using first entry only");
    982                 final KindSectionDataList kindSectionDataList = new KindSectionDataList();
    983                 kindSectionDataList.add(mPrimaryNameKindSectionData.first);
    984                 final CompactKindSectionView kindSectionView = inflateKindSectionView(
    985                         mKindSectionViews, kindSectionDataList, mimeType,
    986                         mPrimaryNameKindSectionData.second);
    987                 mKindSectionViews.addView(kindSectionView);
    988 
    989                 // Keep a pointer to all the KindSectionsViews for each mimeType
    990                 getKindSectionViews(mimeType).add(kindSectionView);
    991             } else {
    992                 final KindSectionDataList kindSectionDataList = entry.getValue();
    993 
    994                 // Ignore mime types that we've already handled
    995                 if (Photo.CONTENT_ITEM_TYPE.equals(mimeType)) {
    996                     vlog("kind: " + i + " " + mimeType + " dropped");
    997                     continue;
    998                 }
    999 
   1000                 // Don't show more than one group editor on the compact editor.
   1001                 // Groups will still be editable for each raw contact individually on the full editor.
   1002                 if (GroupMembership.CONTENT_ITEM_TYPE.equals(mimeType)
   1003                         && kindSectionDataList.size() > 1) {
   1004                     vlog("kind: " + i + " " + mimeType + " dropped");
   1005                     continue;
   1006                 }
   1007 
   1008                 if (kindSectionDataList != null && !kindSectionDataList.isEmpty()) {
   1009                     vlog("kind: " + i + " " + mimeType + " " + kindSectionDataList.size() +
   1010                             " kindSectionData(s)");
   1011 
   1012                     final CompactKindSectionView kindSectionView = inflateKindSectionView(
   1013                             mKindSectionViews, kindSectionDataList, mimeType,
   1014                             /* primaryValueDelta =*/ null);
   1015                     mKindSectionViews.addView(kindSectionView);
   1016 
   1017                     // Keep a pointer to all the KindSectionsViews for each mimeType
   1018                     getKindSectionViews(mimeType).add(kindSectionView);
   1019                 }
   1020             }
   1021         }
   1022     }
   1023 
   1024     private List<CompactKindSectionView> getKindSectionViews(String mimeType) {
   1025         List<CompactKindSectionView> kindSectionViews = mKindSectionViewsMap.get(mimeType);
   1026         if (kindSectionViews == null) {
   1027             kindSectionViews = new ArrayList<>();
   1028             mKindSectionViewsMap.put(mimeType, kindSectionViews);
   1029         }
   1030         return kindSectionViews;
   1031     }
   1032 
   1033     private CompactKindSectionView inflateKindSectionView(ViewGroup viewGroup,
   1034             KindSectionDataList kindSectionDataList, String mimeType,
   1035             ValuesDelta primaryValuesDelta) {
   1036         final CompactKindSectionView kindSectionView = (CompactKindSectionView)
   1037                 mLayoutInflater.inflate(R.layout.compact_item_kind_section, viewGroup,
   1038                         /* attachToRoot =*/ false);
   1039         kindSectionView.setIsUserProfile(mIsUserProfile);
   1040 
   1041         if (Phone.CONTENT_ITEM_TYPE.equals(mimeType)
   1042                 || Email.CONTENT_ITEM_TYPE.equals(mimeType)) {
   1043             // Phone numbers and email addresses are always displayed,
   1044             // even if they are empty
   1045             kindSectionView.setHideWhenEmpty(false);
   1046         }
   1047 
   1048         // Since phone numbers and email addresses displayed even if they are empty,
   1049         // they will be the only types you add new values to initially for new contacts
   1050         kindSectionView.setShowOneEmptyEditor(true);
   1051 
   1052         // Sort non-name editors so they wind up in the order we want
   1053         if (!StructuredName.CONTENT_ITEM_TYPE.equals(mimeType)) {
   1054             Collections.sort(kindSectionDataList, new EditorComparator(getContext()));
   1055         }
   1056 
   1057         kindSectionView.setState(kindSectionDataList, mViewIdGenerator, mListener,
   1058                 primaryValuesDelta);
   1059 
   1060         return kindSectionView;
   1061     }
   1062 
   1063     void maybeSetReadOnlyDisplayNameAsPrimary(String readOnlyDisplayName) {
   1064         if (TextUtils.isEmpty(readOnlyDisplayName)) return;
   1065         final CompactKindSectionView primaryNameKindSectionView = getPrimaryNameKindSectionView();
   1066         if (primaryNameKindSectionView != null && primaryNameKindSectionView.isEmptyName()) {
   1067             vlog("name: using read only display name as primary name");
   1068             primaryNameKindSectionView.setName(readOnlyDisplayName);
   1069         }
   1070     }
   1071 
   1072     private CompactKindSectionView getPrimaryNameKindSectionView() {
   1073         final List<CompactKindSectionView> kindSectionViews
   1074                 = mKindSectionViewsMap.get(StructuredName.CONTENT_ITEM_TYPE);
   1075         return kindSectionViews == null || kindSectionViews.isEmpty()
   1076                 ? null : kindSectionViews.get(0);
   1077     }
   1078 
   1079     private void showAllFields() {
   1080         // Stop hiding empty editors and allow the user to enter values for all kinds now
   1081         for (int i = 0; i < mKindSectionViews.getChildCount(); i++) {
   1082             final CompactKindSectionView kindSectionView =
   1083                     (CompactKindSectionView) mKindSectionViews.getChildAt(i);
   1084             kindSectionView.setHideWhenEmpty(false);
   1085             kindSectionView.updateEmptyEditors(/* shouldAnimate =*/ true);
   1086         }
   1087         mIsExpanded = true;
   1088 
   1089         // Hide the more fields button
   1090         mMoreFields.setVisibility(View.GONE);
   1091     }
   1092 
   1093     private static void vlog(String message) {
   1094         if (Log.isLoggable(TAG, Log.VERBOSE)) {
   1095             Log.v(TAG, message);
   1096         }
   1097     }
   1098 
   1099     private static void wlog(String message) {
   1100         if (Log.isLoggable(TAG, Log.WARN)) {
   1101             Log.w(TAG, message);
   1102         }
   1103     }
   1104 
   1105     private static void elog(String message) {
   1106         Log.e(TAG, message);
   1107     }
   1108 }
   1109