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 android.content.Context;
     20 import android.content.res.Resources;
     21 import android.database.Cursor;
     22 import android.graphics.drawable.Drawable;
     23 import android.net.Uri;
     24 import android.os.Bundle;
     25 import android.os.Parcel;
     26 import android.os.Parcelable;
     27 import android.provider.ContactsContract.CommonDataKinds.Email;
     28 import android.provider.ContactsContract.CommonDataKinds.Event;
     29 import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
     30 import android.provider.ContactsContract.CommonDataKinds.Im;
     31 import android.provider.ContactsContract.CommonDataKinds.Nickname;
     32 import android.provider.ContactsContract.CommonDataKinds.Note;
     33 import android.provider.ContactsContract.CommonDataKinds.Organization;
     34 import android.provider.ContactsContract.CommonDataKinds.Phone;
     35 import android.provider.ContactsContract.CommonDataKinds.Photo;
     36 import android.provider.ContactsContract.CommonDataKinds.Relation;
     37 import android.provider.ContactsContract.CommonDataKinds.SipAddress;
     38 import android.provider.ContactsContract.CommonDataKinds.StructuredName;
     39 import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
     40 import android.provider.ContactsContract.CommonDataKinds.Website;
     41 import android.text.TextUtils;
     42 import android.util.AttributeSet;
     43 import android.util.Log;
     44 import android.view.LayoutInflater;
     45 import android.view.View;
     46 import android.view.ViewGroup;
     47 import android.widget.AdapterView;
     48 import android.widget.ImageView;
     49 import android.widget.LinearLayout;
     50 import android.widget.ListPopupWindow;
     51 import android.widget.TextView;
     52 
     53 import com.android.contacts.GeoUtil;
     54 import com.android.contacts.R;
     55 import com.android.contacts.compat.PhoneNumberUtilsCompat;
     56 import com.android.contacts.model.AccountTypeManager;
     57 import com.android.contacts.model.RawContactDelta;
     58 import com.android.contacts.model.RawContactDeltaList;
     59 import com.android.contacts.model.RawContactModifier;
     60 import com.android.contacts.model.ValuesDelta;
     61 import com.android.contacts.model.account.AccountInfo;
     62 import com.android.contacts.model.account.AccountType;
     63 import com.android.contacts.model.account.AccountWithDataSet;
     64 import com.android.contacts.model.dataitem.CustomDataItem;
     65 import com.android.contacts.model.dataitem.DataKind;
     66 import com.android.contacts.util.AccountsListAdapter;
     67 import com.android.contacts.util.MaterialColorMapUtils;
     68 import com.android.contacts.util.UiClosables;
     69 
     70 import java.io.FileNotFoundException;
     71 import java.util.ArrayList;
     72 import java.util.Arrays;
     73 import java.util.Comparator;
     74 import java.util.HashMap;
     75 import java.util.List;
     76 import java.util.Map;
     77 import java.util.Set;
     78 import java.util.TreeSet;
     79 
     80 /**
     81  * View to display information from multiple {@link RawContactDelta}s grouped together.
     82  */
     83 public class RawContactEditorView extends LinearLayout implements View.OnClickListener {
     84 
     85     static final String TAG = "RawContactEditorView";
     86 
     87     /**
     88      * Callbacks for hosts of {@link RawContactEditorView}s.
     89      */
     90     public interface Listener {
     91 
     92         /**
     93          * Invoked when the structured name editor field has changed.
     94          *
     95          * @param rawContactId The raw contact ID from the underlying {@link RawContactDelta}.
     96          * @param valuesDelta The values from the underlying {@link RawContactDelta}.
     97          */
     98         public void onNameFieldChanged(long rawContactId, ValuesDelta valuesDelta);
     99 
    100         /**
    101          * Invoked when the editor should rebind editors for a new account.
    102          *
    103          * @param oldState Old data being edited.
    104          * @param oldAccount Old account associated with oldState.
    105          * @param newAccount New account to be used.
    106          */
    107         public void onRebindEditorsForNewContact(RawContactDelta oldState,
    108                 AccountWithDataSet oldAccount, AccountWithDataSet newAccount);
    109 
    110         /**
    111          * Invoked when no editors could be bound for the contact.
    112          */
    113         public void onBindEditorsFailed();
    114 
    115         /**
    116          * Invoked after editors have been bound for the contact.
    117          */
    118         public void onEditorsBound();
    119     }
    120     /**
    121      * Sorts kinds roughly the same as quick contacts; we diverge in the following ways:
    122      * <ol>
    123      *     <li>All names are together at the top.</li>
    124      *     <li>IM is moved up after addresses</li>
    125      *     <li>SIP addresses are moved to below phone numbers</li>
    126      *     <li>Group membership is placed at the end</li>
    127      * </ol>
    128      */
    129     private static final class MimeTypeComparator implements Comparator<String> {
    130 
    131         private static final List<String> MIME_TYPE_ORDER = Arrays.asList(new String[] {
    132                 StructuredName.CONTENT_ITEM_TYPE,
    133                 Nickname.CONTENT_ITEM_TYPE,
    134                 Organization.CONTENT_ITEM_TYPE,
    135                 Phone.CONTENT_ITEM_TYPE,
    136                 SipAddress.CONTENT_ITEM_TYPE,
    137                 Email.CONTENT_ITEM_TYPE,
    138                 StructuredPostal.CONTENT_ITEM_TYPE,
    139                 Im.CONTENT_ITEM_TYPE,
    140                 Website.CONTENT_ITEM_TYPE,
    141                 Event.CONTENT_ITEM_TYPE,
    142                 Relation.CONTENT_ITEM_TYPE,
    143                 Note.CONTENT_ITEM_TYPE,
    144                 GroupMembership.CONTENT_ITEM_TYPE
    145         });
    146 
    147         @Override
    148         public int compare(String mimeType1, String mimeType2) {
    149             if (mimeType1 == mimeType2) return 0;
    150             if (mimeType1 == null) return -1;
    151             if (mimeType2 == null) return 1;
    152 
    153             int index1 = MIME_TYPE_ORDER.indexOf(mimeType1);
    154             int index2 = MIME_TYPE_ORDER.indexOf(mimeType2);
    155 
    156             // Fallback to alphabetical ordering of the mime type if both are not found
    157             if (index1 < 0 && index2 < 0) return mimeType1.compareTo(mimeType2);
    158             if (index1 < 0) return 1;
    159             if (index2 < 0) return -1;
    160 
    161             return index1 < index2 ? -1 : 1;
    162         }
    163     }
    164 
    165     public static class SavedState extends BaseSavedState {
    166 
    167         public static final Parcelable.Creator<SavedState> CREATOR =
    168                 new Parcelable.Creator<SavedState>() {
    169                     public SavedState createFromParcel(Parcel in) {
    170                         return new SavedState(in);
    171                     }
    172                     public SavedState[] newArray(int size) {
    173                         return new SavedState[size];
    174                     }
    175                 };
    176 
    177         private boolean mIsExpanded;
    178 
    179         public SavedState(Parcelable superState) {
    180             super(superState);
    181         }
    182 
    183         private SavedState(Parcel in) {
    184             super(in);
    185             mIsExpanded = in.readInt() != 0;
    186         }
    187 
    188         @Override
    189         public void writeToParcel(Parcel out, int flags) {
    190             super.writeToParcel(out, flags);
    191             out.writeInt(mIsExpanded ? 1 : 0);
    192         }
    193     }
    194 
    195     private RawContactEditorView.Listener mListener;
    196 
    197     private AccountTypeManager mAccountTypeManager;
    198     private LayoutInflater mLayoutInflater;
    199 
    200     private ViewIdGenerator mViewIdGenerator;
    201     private MaterialColorMapUtils.MaterialPalette mMaterialPalette;
    202     private boolean mHasNewContact;
    203     private boolean mIsUserProfile;
    204     private AccountWithDataSet mPrimaryAccount;
    205     private List<AccountInfo> mAccounts = new ArrayList<>();
    206     private RawContactDeltaList mRawContactDeltas;
    207     private RawContactDelta mCurrentRawContactDelta;
    208     private long mRawContactIdToDisplayAlone = -1;
    209     private Map<String, KindSectionData> mKindSectionDataMap = new HashMap<>();
    210     private Set<String> mSortedMimetypes = new TreeSet<>(new MimeTypeComparator());
    211 
    212     // Account header
    213     private View mAccountHeaderContainer;
    214     private TextView mAccountHeaderPrimaryText;
    215     private TextView mAccountHeaderSecondaryText;
    216     private ImageView mAccountHeaderIcon;
    217     private ImageView mAccountHeaderExpanderIcon;
    218 
    219     private PhotoEditorView mPhotoView;
    220     private ViewGroup mKindSectionViews;
    221     private Map<String, KindSectionView> mKindSectionViewMap = new HashMap<>();
    222     private View mMoreFields;
    223 
    224     private boolean mIsExpanded;
    225 
    226     private Bundle mIntentExtras;
    227 
    228     private ValuesDelta mPhotoValuesDelta;
    229 
    230     public RawContactEditorView(Context context) {
    231         super(context);
    232     }
    233 
    234     public RawContactEditorView(Context context, AttributeSet attrs) {
    235         super(context, attrs);
    236     }
    237 
    238     /**
    239      * Sets the receiver for {@link RawContactEditorView} callbacks.
    240      */
    241     public void setListener(Listener listener) {
    242         mListener = listener;
    243     }
    244 
    245     @Override
    246     protected void onFinishInflate() {
    247         super.onFinishInflate();
    248 
    249         mAccountTypeManager = AccountTypeManager.getInstance(getContext());
    250         mLayoutInflater = (LayoutInflater)
    251                 getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    252 
    253         // Account header
    254         mAccountHeaderContainer = findViewById(R.id.account_header_container);
    255         mAccountHeaderPrimaryText = (TextView) findViewById(R.id.account_type);
    256         mAccountHeaderSecondaryText = (TextView) findViewById(R.id.account_name);
    257         mAccountHeaderIcon = (ImageView) findViewById(R.id.account_type_icon);
    258         mAccountHeaderExpanderIcon = (ImageView) findViewById(R.id.account_expander_icon);
    259 
    260         mPhotoView = (PhotoEditorView) findViewById(R.id.photo_editor);
    261         mKindSectionViews = (LinearLayout) findViewById(R.id.kind_section_views);
    262         mMoreFields = findViewById(R.id.more_fields);
    263         mMoreFields.setOnClickListener(this);
    264     }
    265 
    266     @Override
    267     public void onClick(View view) {
    268         if (view.getId() == R.id.more_fields) {
    269             showAllFields();
    270         }
    271     }
    272 
    273     @Override
    274     public void setEnabled(boolean enabled) {
    275         super.setEnabled(enabled);
    276         final int childCount = mKindSectionViews.getChildCount();
    277         for (int i = 0; i < childCount; i++) {
    278             mKindSectionViews.getChildAt(i).setEnabled(enabled);
    279         }
    280     }
    281 
    282     @Override
    283     public Parcelable onSaveInstanceState() {
    284         final Parcelable superState = super.onSaveInstanceState();
    285         final SavedState savedState = new SavedState(superState);
    286         savedState.mIsExpanded = mIsExpanded;
    287         return savedState;
    288     }
    289 
    290     @Override
    291     public void onRestoreInstanceState(Parcelable state) {
    292         if(!(state instanceof SavedState)) {
    293             super.onRestoreInstanceState(state);
    294             return;
    295         }
    296         final SavedState savedState = (SavedState) state;
    297         super.onRestoreInstanceState(savedState.getSuperState());
    298         mIsExpanded = savedState.mIsExpanded;
    299         if (mIsExpanded) {
    300             showAllFields();
    301         }
    302     }
    303 
    304     /**
    305      * Pass through to {@link PhotoEditorView#setListener}.
    306      */
    307     public void setPhotoListener(PhotoEditorView.Listener listener) {
    308         mPhotoView.setListener(listener);
    309     }
    310 
    311     public void removePhoto() {
    312         mPhotoValuesDelta.setFromTemplate(true);
    313         mPhotoValuesDelta.put(Photo.PHOTO, (byte[]) null);
    314         mPhotoValuesDelta.put(Photo.PHOTO_FILE_ID, (String) null);
    315 
    316         mPhotoView.removePhoto();
    317     }
    318 
    319     /**
    320      * Pass through to {@link PhotoEditorView#setFullSizedPhoto(Uri)}.
    321      */
    322     public void setFullSizePhoto(Uri photoUri) {
    323         mPhotoView.setFullSizedPhoto(photoUri);
    324     }
    325 
    326     public void updatePhoto(Uri photoUri) {
    327         mPhotoValuesDelta.setFromTemplate(false);
    328         // Unset primary for all photos
    329         unsetSuperPrimaryFromAllPhotos();
    330         // Mark the currently displayed photo as primary
    331         mPhotoValuesDelta.setSuperPrimary(true);
    332 
    333         // Even though high-res photos cannot be saved by passing them via
    334         // an EntityDeltaList (since they cause the Bundle size limit to be
    335         // exceeded), we still pass a low-res thumbnail. This simplifies
    336         // code all over the place, because we don't have to test whether
    337         // there is a change in EITHER the delta-list OR a changed photo...
    338         // this way, there is always a change in the delta-list.
    339         try {
    340             final byte[] bytes = EditorUiUtils.getCompressedThumbnailBitmapBytes(
    341                     getContext(), photoUri);
    342             if (bytes != null) {
    343                 mPhotoValuesDelta.setPhoto(bytes);
    344             }
    345         } catch (FileNotFoundException e) {
    346             elog("Failed to get bitmap from photo Uri");
    347         }
    348 
    349         mPhotoView.setFullSizedPhoto(photoUri);
    350     }
    351 
    352     private void unsetSuperPrimaryFromAllPhotos() {
    353         for (int i = 0; i < mRawContactDeltas.size(); i++) {
    354             final RawContactDelta rawContactDelta = mRawContactDeltas.get(i);
    355             if (!rawContactDelta.hasMimeEntries(Photo.CONTENT_ITEM_TYPE)) {
    356                 continue;
    357             }
    358             final List<ValuesDelta> photosDeltas =
    359                     mRawContactDeltas.get(i).getMimeEntries(Photo.CONTENT_ITEM_TYPE);
    360             if (photosDeltas == null) {
    361                 continue;
    362             }
    363             for (int j = 0; j < photosDeltas.size(); j++) {
    364                 photosDeltas.get(j).setSuperPrimary(false);
    365             }
    366         }
    367     }
    368 
    369     /**
    370      * Pass through to {@link PhotoEditorView#isWritablePhotoSet}.
    371      */
    372     public boolean isWritablePhotoSet() {
    373         return mPhotoView.isWritablePhotoSet();
    374     }
    375 
    376     /**
    377      * Get the raw contact ID for the current photo.
    378      */
    379     public long getPhotoRawContactId() {
    380         return mCurrentRawContactDelta == null ? - 1 : mCurrentRawContactDelta.getRawContactId();
    381     }
    382 
    383     public StructuredNameEditorView getNameEditorView() {
    384         final KindSectionView nameKindSectionView = mKindSectionViewMap
    385                 .get(StructuredName.CONTENT_ITEM_TYPE);
    386         return nameKindSectionView == null
    387                 ? null : nameKindSectionView.getNameEditorView();
    388     }
    389 
    390     public RawContactDelta getCurrentRawContactDelta() {
    391         return mCurrentRawContactDelta;
    392     }
    393 
    394     /**
    395      * Marks the raw contact photo given as primary for the aggregate contact.
    396      */
    397     public void setPrimaryPhoto() {
    398 
    399         // Update values delta
    400         final ValuesDelta valuesDelta = mCurrentRawContactDelta
    401                 .getSuperPrimaryEntry(Photo.CONTENT_ITEM_TYPE);
    402         if (valuesDelta == null) {
    403             Log.wtf(TAG, "setPrimaryPhoto: had no ValuesDelta for the current RawContactDelta");
    404             return;
    405         }
    406         valuesDelta.setFromTemplate(false);
    407         unsetSuperPrimaryFromAllPhotos();
    408         valuesDelta.setSuperPrimary(true);
    409     }
    410 
    411     public View getAggregationAnchorView() {
    412         final StructuredNameEditorView nameEditorView = getNameEditorView();
    413         return nameEditorView != null ? nameEditorView.findViewById(R.id.anchor_view) : null;
    414     }
    415 
    416     public void setGroupMetaData(Cursor groupMetaData) {
    417         final KindSectionView groupKindSectionView =
    418                 mKindSectionViewMap.get(GroupMembership.CONTENT_ITEM_TYPE);
    419         if (groupKindSectionView == null) {
    420             return;
    421         }
    422         groupKindSectionView.setGroupMetaData(groupMetaData);
    423         if (mIsExpanded) {
    424             groupKindSectionView.setHideWhenEmpty(false);
    425             groupKindSectionView.updateEmptyEditors(/* shouldAnimate =*/ true);
    426         }
    427     }
    428 
    429     public void setIntentExtras(Bundle extras) {
    430         mIntentExtras = extras;
    431     }
    432 
    433     public void setState(RawContactDeltaList rawContactDeltas,
    434             MaterialColorMapUtils.MaterialPalette materialPalette, ViewIdGenerator viewIdGenerator,
    435             boolean hasNewContact, boolean isUserProfile, AccountWithDataSet primaryAccount,
    436             long rawContactIdToDisplayAlone) {
    437 
    438         mRawContactDeltas = rawContactDeltas;
    439         mRawContactIdToDisplayAlone = rawContactIdToDisplayAlone;
    440 
    441         mKindSectionViewMap.clear();
    442         mKindSectionViews.removeAllViews();
    443         mMoreFields.setVisibility(View.VISIBLE);
    444 
    445         mMaterialPalette = materialPalette;
    446         mViewIdGenerator = viewIdGenerator;
    447 
    448         mHasNewContact = hasNewContact;
    449         mIsUserProfile = isUserProfile;
    450         mPrimaryAccount = primaryAccount;
    451         if (mPrimaryAccount == null && mAccounts != null) {
    452             mPrimaryAccount = ContactEditorUtils.create(getContext())
    453                     .getOnlyOrDefaultAccount(AccountInfo.extractAccounts(mAccounts));
    454         }
    455         if (Log.isLoggable(TAG, Log.VERBOSE)) {
    456             Log.v(TAG, "state: primary " + mPrimaryAccount);
    457         }
    458 
    459         // Parse the given raw contact deltas
    460         if (rawContactDeltas == null || rawContactDeltas.isEmpty()) {
    461             elog("No raw contact deltas");
    462             if (mListener != null) mListener.onBindEditorsFailed();
    463             return;
    464         }
    465         pickRawContactDelta();
    466         if (mCurrentRawContactDelta == null) {
    467             elog("Couldn't pick a raw contact delta.");
    468             if (mListener != null) mListener.onBindEditorsFailed();
    469             return;
    470         }
    471         // Apply any intent extras now that we have selected a raw contact delta.
    472         applyIntentExtras();
    473         parseRawContactDelta();
    474         if (mKindSectionDataMap.isEmpty()) {
    475             elog("No kind section data parsed from RawContactDelta(s)");
    476             if (mListener != null) mListener.onBindEditorsFailed();
    477             return;
    478         }
    479 
    480         final KindSectionData nameSectionData =
    481                 mKindSectionDataMap.get(StructuredName.CONTENT_ITEM_TYPE);
    482         // Ensure that a structured name and photo exists
    483         if (nameSectionData != null) {
    484             final RawContactDelta rawContactDelta =
    485                     nameSectionData.getRawContactDelta();
    486             RawContactModifier.ensureKindExists(
    487                     rawContactDelta,
    488                     rawContactDelta.getAccountType(mAccountTypeManager),
    489                     StructuredName.CONTENT_ITEM_TYPE);
    490             RawContactModifier.ensureKindExists(
    491                     rawContactDelta,
    492                     rawContactDelta.getAccountType(mAccountTypeManager),
    493                     Photo.CONTENT_ITEM_TYPE);
    494         }
    495 
    496         // Setup the view
    497         addPhotoView();
    498         setAccountInfo();
    499         if (isReadOnlyRawContact()) {
    500             // We're want to display the inputs fields for a single read only raw contact
    501             addReadOnlyRawContactEditorViews();
    502         } else {
    503             setupEditorNormally();
    504             // If we're inserting a new contact, request focus to bring up the keyboard for the
    505             // name field.
    506             if (mHasNewContact) {
    507                 final StructuredNameEditorView name = getNameEditorView();
    508                 if (name != null) {
    509                     name.requestFocusForFirstEditField();
    510                 }
    511             }
    512         }
    513         if (mListener != null) mListener.onEditorsBound();
    514     }
    515 
    516     public void setAccounts(List<AccountInfo> accounts) {
    517         mAccounts.clear();
    518         mAccounts.addAll(accounts);
    519         // Update the account header
    520         setAccountInfo();
    521     }
    522 
    523     private void setupEditorNormally() {
    524         addKindSectionViews();
    525 
    526         mMoreFields.setVisibility(hasMoreFields() ? View.VISIBLE : View.GONE);
    527 
    528         if (mIsExpanded) showAllFields();
    529     }
    530 
    531     private boolean isReadOnlyRawContact() {
    532         return !mCurrentRawContactDelta.getAccountType(mAccountTypeManager).areContactsWritable();
    533     }
    534 
    535     private void pickRawContactDelta() {
    536         if (Log.isLoggable(TAG, Log.VERBOSE)) {
    537             Log.v(TAG, "parse: " + mRawContactDeltas.size() + " rawContactDelta(s)");
    538         }
    539         for (int j = 0; j < mRawContactDeltas.size(); j++) {
    540             final RawContactDelta rawContactDelta = mRawContactDeltas.get(j);
    541             if (Log.isLoggable(TAG, Log.VERBOSE)) {
    542                 Log.v(TAG, "parse: " + j + " rawContactDelta" + rawContactDelta);
    543             }
    544             if (rawContactDelta == null || !rawContactDelta.isVisible()) continue;
    545             final AccountType accountType = rawContactDelta.getAccountType(mAccountTypeManager);
    546             if (accountType == null) continue;
    547 
    548             if (mRawContactIdToDisplayAlone > 0) {
    549                 // Look for the raw contact if specified.
    550                 if (rawContactDelta.getRawContactId().equals(mRawContactIdToDisplayAlone)) {
    551                     mCurrentRawContactDelta = rawContactDelta;
    552                     return;
    553                 }
    554             } else if (mPrimaryAccount != null
    555                     && mPrimaryAccount.equals(rawContactDelta.getAccountWithDataSet())) {
    556                 // Otherwise try to find the one that matches the default.
    557                 mCurrentRawContactDelta = rawContactDelta;
    558                 return;
    559             } else if (accountType.areContactsWritable()){
    560                 // TODO: Find better raw contact delta
    561                 // Just select an arbitrary writable contact.
    562                 mCurrentRawContactDelta = rawContactDelta;
    563             }
    564         }
    565 
    566     }
    567 
    568     private void applyIntentExtras() {
    569         if (mIntentExtras == null || mIntentExtras.size() == 0) {
    570             return;
    571         }
    572         final AccountTypeManager accountTypes = AccountTypeManager.getInstance(getContext());
    573         final AccountType type = mCurrentRawContactDelta.getAccountType(accountTypes);
    574 
    575         RawContactModifier.parseExtras(getContext(), type, mCurrentRawContactDelta, mIntentExtras);
    576         mIntentExtras = null;
    577     }
    578 
    579     private void parseRawContactDelta() {
    580         mKindSectionDataMap.clear();
    581         mSortedMimetypes.clear();
    582 
    583         final AccountType accountType = mCurrentRawContactDelta.getAccountType(mAccountTypeManager);
    584         final List<DataKind> dataKinds = accountType.getSortedDataKinds();
    585         final int dataKindSize = dataKinds == null ? 0 : dataKinds.size();
    586         if (Log.isLoggable(TAG, Log.VERBOSE)) {
    587             Log.v(TAG, "parse: " + dataKindSize + " dataKinds(s)");
    588         }
    589 
    590         for (int i = 0; i < dataKindSize; i++) {
    591             final DataKind dataKind = dataKinds.get(i);
    592             // Skip null and un-editable fields.
    593             if (dataKind == null || !dataKind.editable) {
    594                 if (Log.isLoggable(TAG, Log.VERBOSE)) {
    595                     Log.v(TAG, "parse: " + i +
    596                             (dataKind == null ? " dropped null data kind"
    597                                     : " dropped uneditable mimetype: " + dataKind.mimeType));
    598                 }
    599                 continue;
    600             }
    601             final String mimeType = dataKind.mimeType;
    602 
    603             // Skip psuedo mime types
    604             if (DataKind.PSEUDO_MIME_TYPE_NAME.equals(mimeType) ||
    605                     DataKind.PSEUDO_MIME_TYPE_PHONETIC_NAME.equals(mimeType)) {
    606                 if (Log.isLoggable(TAG, Log.VERBOSE)) {
    607                     Log.v(TAG, "parse: " + i + " " + dataKind.mimeType + " dropped pseudo type");
    608                 }
    609                 continue;
    610             }
    611 
    612             // Skip custom fields
    613             // TODO: Handle them when we implement editing custom fields.
    614             if (CustomDataItem.MIMETYPE_CUSTOM_FIELD.equals(mimeType)) {
    615                 if (Log.isLoggable(TAG, Log.VERBOSE)) {
    616                     Log.v(TAG, "parse: " + i + " " + dataKind.mimeType + " dropped custom field");
    617                 }
    618                 continue;
    619             }
    620 
    621             final KindSectionData kindSectionData =
    622                     new KindSectionData(accountType, dataKind, mCurrentRawContactDelta);
    623             mKindSectionDataMap.put(mimeType, kindSectionData);
    624             mSortedMimetypes.add(mimeType);
    625 
    626             if (Log.isLoggable(TAG, Log.VERBOSE)) {
    627                 Log.v(TAG, "parse: " + i + " " + dataKind.mimeType + " " +
    628                         kindSectionData.getValuesDeltas().size() + " value(s) " +
    629                         kindSectionData.getNonEmptyValuesDeltas().size() + " non-empty value(s) " +
    630                         kindSectionData.getVisibleValuesDeltas().size() +
    631                         " visible value(s)");
    632             }
    633         }
    634     }
    635 
    636     private void addReadOnlyRawContactEditorViews() {
    637         mKindSectionViews.removeAllViews();
    638         final AccountTypeManager accountTypes = AccountTypeManager.getInstance(
    639                 getContext());
    640         final AccountType type = mCurrentRawContactDelta.getAccountType(accountTypes);
    641 
    642         // Bail if invalid state or source
    643         if (type == null) return;
    644 
    645         // Make sure we have StructuredName
    646         RawContactModifier.ensureKindExists(
    647                 mCurrentRawContactDelta, type, StructuredName.CONTENT_ITEM_TYPE);
    648 
    649         ValuesDelta primary;
    650 
    651         // Name
    652         final Context context = getContext();
    653         final Resources res = context.getResources();
    654         primary = mCurrentRawContactDelta.getPrimaryEntry(StructuredName.CONTENT_ITEM_TYPE);
    655         final String name = primary != null ? primary.getAsString(StructuredName.DISPLAY_NAME) :
    656             getContext().getString(R.string.missing_name);
    657         final Drawable nameDrawable = context.getDrawable(R.drawable.quantum_ic_person_vd_theme_24);
    658         final String nameContentDescription = res.getString(R.string.header_name_entry);
    659         bindData(nameDrawable, nameContentDescription, name, /* type */ null,
    660                 /* isFirstEntry */ true);
    661 
    662         // Phones
    663         final ArrayList<ValuesDelta> phones = mCurrentRawContactDelta
    664                 .getMimeEntries(Phone.CONTENT_ITEM_TYPE);
    665         final Drawable phoneDrawable = context.getDrawable(R.drawable.quantum_ic_phone_vd_theme_24);
    666         final String phoneContentDescription = res.getString(R.string.header_phone_entry);
    667         if (phones != null) {
    668             boolean isFirstPhoneBound = true;
    669             for (ValuesDelta phone : phones) {
    670                 final String phoneNumber = phone.getPhoneNumber();
    671                 if (TextUtils.isEmpty(phoneNumber)) {
    672                     continue;
    673                 }
    674                 final String formattedNumber = PhoneNumberUtilsCompat.formatNumber(
    675                         phoneNumber, phone.getPhoneNormalizedNumber(),
    676                         GeoUtil.getCurrentCountryIso(getContext()));
    677                 CharSequence phoneType = null;
    678                 if (phone.hasPhoneType()) {
    679                     phoneType = Phone.getTypeLabel(
    680                             res, phone.getPhoneType(), phone.getPhoneLabel());
    681                 }
    682                 bindData(phoneDrawable, phoneContentDescription, formattedNumber, phoneType,
    683                         isFirstPhoneBound, true);
    684                 isFirstPhoneBound = false;
    685             }
    686         }
    687 
    688         // Emails
    689         final ArrayList<ValuesDelta> emails = mCurrentRawContactDelta
    690                 .getMimeEntries(Email.CONTENT_ITEM_TYPE);
    691         final Drawable emailDrawable = context.getDrawable(R.drawable.quantum_ic_email_vd_theme_24);
    692         final String emailContentDescription = res.getString(R.string.header_email_entry);
    693         if (emails != null) {
    694             boolean isFirstEmailBound = true;
    695             for (ValuesDelta email : emails) {
    696                 final String emailAddress = email.getEmailData();
    697                 if (TextUtils.isEmpty(emailAddress)) {
    698                     continue;
    699                 }
    700                 CharSequence emailType = null;
    701                 if (email.hasEmailType()) {
    702                     emailType = Email.getTypeLabel(
    703                             res, email.getEmailType(), email.getEmailLabel());
    704                 }
    705                 bindData(emailDrawable, emailContentDescription, emailAddress, emailType,
    706                         isFirstEmailBound);
    707                 isFirstEmailBound = false;
    708             }
    709         }
    710 
    711         mKindSectionViews.setVisibility(mKindSectionViews.getChildCount() > 0 ? VISIBLE : GONE);
    712         // Hide the "More fields" link
    713         mMoreFields.setVisibility(GONE);
    714     }
    715 
    716     private void bindData(Drawable icon, String iconContentDescription, CharSequence data,
    717             CharSequence type, boolean isFirstEntry) {
    718         bindData(icon, iconContentDescription, data, type, isFirstEntry, false);
    719     }
    720 
    721     private void bindData(Drawable icon, String iconContentDescription, CharSequence data,
    722             CharSequence type, boolean isFirstEntry, boolean forceLTR) {
    723         final View field = mLayoutInflater.inflate(R.layout.item_read_only_field, mKindSectionViews,
    724                 /* attachToRoot */ false);
    725         if (isFirstEntry) {
    726             final ImageView imageView = (ImageView) field.findViewById(R.id.kind_icon);
    727             imageView.setImageDrawable(icon);
    728             imageView.setContentDescription(iconContentDescription);
    729         } else {
    730             final ImageView imageView = (ImageView) field.findViewById(R.id.kind_icon);
    731             imageView.setVisibility(View.INVISIBLE);
    732             imageView.setContentDescription(null);
    733         }
    734         final TextView dataView = (TextView) field.findViewById(R.id.data);
    735         dataView.setText(data);
    736         if (forceLTR) {
    737             dataView.setTextDirection(View.TEXT_DIRECTION_LTR);
    738         }
    739         final TextView typeView = (TextView) field.findViewById(R.id.type);
    740         if (!TextUtils.isEmpty(type)) {
    741             typeView.setText(type);
    742         } else {
    743             typeView.setVisibility(View.GONE);
    744         }
    745         mKindSectionViews.addView(field);
    746     }
    747 
    748     private void setAccountInfo() {
    749         if (mCurrentRawContactDelta == null && mPrimaryAccount == null) {
    750             return;
    751         }
    752         final AccountTypeManager accountTypeManager = AccountTypeManager.getInstance(getContext());
    753         final AccountInfo account = mCurrentRawContactDelta != null
    754                 ? accountTypeManager.getAccountInfoForAccount(
    755                 mCurrentRawContactDelta.getAccountWithDataSet())
    756                 : accountTypeManager.getAccountInfoForAccount(mPrimaryAccount);
    757 
    758         // Accounts haven't loaded yet or we are editing.
    759         if (mAccounts.isEmpty()) {
    760             mAccounts.add(account);
    761         }
    762 
    763         // Get the account information for the primary raw contact delta
    764         if (isReadOnlyRawContact()) {
    765             final String accountType = account.getTypeLabel().toString();
    766             setAccountHeader(accountType,
    767                     getResources().getString(
    768                             R.string.editor_account_selector_read_only_title, accountType));
    769         } else {
    770             final String accountLabel = mIsUserProfile
    771                     ? EditorUiUtils.getAccountHeaderLabelForMyProfile(getContext(), account)
    772                     : account.getNameLabel().toString();
    773             setAccountHeader(getResources().getString(R.string.editor_account_selector_title),
    774                     accountLabel);
    775         }
    776 
    777         // If we're saving a new contact and there are multiple accounts, add the account selector.
    778         if (mHasNewContact && !mIsUserProfile && mAccounts.size() > 1) {
    779             addAccountSelector(mCurrentRawContactDelta);
    780         }
    781     }
    782 
    783     private void setAccountHeader(String primaryText, String secondaryText) {
    784         mAccountHeaderPrimaryText.setText(primaryText);
    785         mAccountHeaderSecondaryText.setText(secondaryText);
    786 
    787         // Set the icon
    788         final AccountType accountType =
    789                 mCurrentRawContactDelta.getRawContactAccountType(getContext());
    790         mAccountHeaderIcon.setImageDrawable(accountType.getDisplayIcon(getContext()));
    791 
    792         // Set the content description
    793         mAccountHeaderContainer.setContentDescription(
    794                 EditorUiUtils.getAccountInfoContentDescription(secondaryText, primaryText));
    795     }
    796 
    797     private void addAccountSelector(final RawContactDelta rawContactDelta) {
    798         // Add handlers for choosing another account to save to.
    799         mAccountHeaderExpanderIcon.setVisibility(View.VISIBLE);
    800         final OnClickListener clickListener = new OnClickListener() {
    801             @Override
    802             public void onClick(View v) {
    803                 final AccountWithDataSet current = rawContactDelta.getAccountWithDataSet();
    804                 AccountInfo.sortAccounts(current, mAccounts);
    805                 final ListPopupWindow popup = new ListPopupWindow(getContext(), null);
    806                 final AccountsListAdapter adapter =
    807                         new AccountsListAdapter(getContext(), mAccounts, current);
    808                 popup.setWidth(mAccountHeaderContainer.getWidth());
    809                 popup.setAnchorView(mAccountHeaderContainer);
    810                 popup.setAdapter(adapter);
    811                 popup.setModal(true);
    812                 popup.setInputMethodMode(ListPopupWindow.INPUT_METHOD_NOT_NEEDED);
    813                 popup.setOnItemClickListener(new AdapterView.OnItemClickListener() {
    814                     @Override
    815                     public void onItemClick(AdapterView<?> parent, View view, int position,
    816                             long id) {
    817                         UiClosables.closeQuietly(popup);
    818                         final AccountWithDataSet newAccount = adapter.getItem(position);
    819                         if (mListener != null && !mPrimaryAccount.equals(newAccount)) {
    820                             mIsExpanded = false;
    821                             mListener.onRebindEditorsForNewContact(
    822                                     rawContactDelta,
    823                                     mPrimaryAccount,
    824                                     newAccount);
    825                         }
    826                     }
    827                 });
    828                 popup.show();
    829             }
    830         };
    831         mAccountHeaderContainer.setOnClickListener(clickListener);
    832         // Make the expander icon clickable so that it will be announced as a button by
    833         // talkback
    834         mAccountHeaderExpanderIcon.setOnClickListener(clickListener);
    835     }
    836 
    837     private void addPhotoView() {
    838         if (!mCurrentRawContactDelta.hasMimeEntries(Photo.CONTENT_ITEM_TYPE)) {
    839             wlog("No photo mimetype for this raw contact.");
    840             mPhotoView.setVisibility(GONE);
    841             return;
    842         } else {
    843             mPhotoView.setVisibility(VISIBLE);
    844         }
    845 
    846         final ValuesDelta superPrimaryDelta = mCurrentRawContactDelta
    847                 .getSuperPrimaryEntry(Photo.CONTENT_ITEM_TYPE);
    848         if (superPrimaryDelta == null) {
    849             Log.wtf(TAG, "addPhotoView: no ValueDelta found for current RawContactDelta"
    850                     + "that supports a photo.");
    851             mPhotoView.setVisibility(GONE);
    852             return;
    853         }
    854         // Set the photo view
    855         mPhotoView.setPalette(mMaterialPalette);
    856         mPhotoView.setPhoto(superPrimaryDelta);
    857 
    858         if (isReadOnlyRawContact()) {
    859             mPhotoView.setReadOnly(true);
    860             return;
    861         }
    862         mPhotoView.setReadOnly(false);
    863         mPhotoValuesDelta = superPrimaryDelta;
    864     }
    865 
    866     private void addKindSectionViews() {
    867         int i = -1;
    868 
    869         for (String mimeType : mSortedMimetypes) {
    870             i++;
    871             // Ignore mime types that we've already handled
    872             if (Photo.CONTENT_ITEM_TYPE.equals(mimeType)) {
    873                 if (Log.isLoggable(TAG, Log.VERBOSE)) {
    874                     Log.v(TAG, "kind: " + i + " " + mimeType + " dropped");
    875                 }
    876                 continue;
    877             }
    878             final KindSectionView kindSectionView;
    879             final KindSectionData kindSectionData = mKindSectionDataMap.get(mimeType);
    880             kindSectionView = inflateKindSectionView(mKindSectionViews, kindSectionData, mimeType);
    881             mKindSectionViews.addView(kindSectionView);
    882 
    883             // Keep a pointer to the KindSectionView for each mimeType
    884             mKindSectionViewMap.put(mimeType, kindSectionView);
    885         }
    886     }
    887 
    888     private KindSectionView inflateKindSectionView(ViewGroup viewGroup,
    889             KindSectionData kindSectionData, String mimeType) {
    890         final KindSectionView kindSectionView = (KindSectionView)
    891                 mLayoutInflater.inflate(R.layout.item_kind_section, viewGroup,
    892                         /* attachToRoot =*/ false);
    893         kindSectionView.setIsUserProfile(mIsUserProfile);
    894 
    895         if (Phone.CONTENT_ITEM_TYPE.equals(mimeType)
    896                 || Email.CONTENT_ITEM_TYPE.equals(mimeType)) {
    897             // Phone numbers and email addresses are always displayed,
    898             // even if they are empty
    899             kindSectionView.setHideWhenEmpty(false);
    900         }
    901 
    902         // Since phone numbers and email addresses displayed even if they are empty,
    903         // they will be the only types you add new values to initially for new contacts
    904         kindSectionView.setShowOneEmptyEditor(true);
    905 
    906         kindSectionView.setState(kindSectionData, mViewIdGenerator, mListener);
    907 
    908         return kindSectionView;
    909     }
    910 
    911     private void showAllFields() {
    912         // Stop hiding empty editors and allow the user to enter values for all kinds now
    913         for (int i = 0; i < mKindSectionViews.getChildCount(); i++) {
    914             final KindSectionView kindSectionView =
    915                     (KindSectionView) mKindSectionViews.getChildAt(i);
    916             kindSectionView.setHideWhenEmpty(false);
    917             kindSectionView.updateEmptyEditors(/* shouldAnimate =*/ true);
    918         }
    919         mIsExpanded = true;
    920 
    921         // Hide the more fields button
    922         mMoreFields.setVisibility(View.GONE);
    923     }
    924 
    925     private boolean hasMoreFields() {
    926         for (KindSectionView section : mKindSectionViewMap.values()) {
    927             if (section.getVisibility() != View.VISIBLE) {
    928                 return true;
    929             }
    930         }
    931         return false;
    932     }
    933 
    934     private static void wlog(String message) {
    935         if (Log.isLoggable(TAG, Log.WARN)) {
    936             Log.w(TAG, message);
    937         }
    938     }
    939 
    940     private static void elog(String message) {
    941         Log.e(TAG, message);
    942     }
    943 }
    944