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 TextFieldsEditorView getPhoneticEditorView() {
391         final KindSectionView kindSectionView = mKindSectionViewMap
392                 .get(StructuredName.CONTENT_ITEM_TYPE);
393         return kindSectionView == null
394                 ? null : kindSectionView.getPhoneticEditorView();
395     }
396 
397     public RawContactDelta getCurrentRawContactDelta() {
398         return mCurrentRawContactDelta;
399     }
400 
401     /**
402      * Marks the raw contact photo given as primary for the aggregate contact.
403      */
404     public void setPrimaryPhoto() {
405 
406         // Update values delta
407         final ValuesDelta valuesDelta = mCurrentRawContactDelta
408                 .getSuperPrimaryEntry(Photo.CONTENT_ITEM_TYPE);
409         if (valuesDelta == null) {
410             Log.wtf(TAG, "setPrimaryPhoto: had no ValuesDelta for the current RawContactDelta");
411             return;
412         }
413         valuesDelta.setFromTemplate(false);
414         unsetSuperPrimaryFromAllPhotos();
415         valuesDelta.setSuperPrimary(true);
416     }
417 
418     public View getAggregationAnchorView() {
419         final StructuredNameEditorView nameEditorView = getNameEditorView();
420         return nameEditorView != null ? nameEditorView.findViewById(R.id.anchor_view) : null;
421     }
422 
423     public void setGroupMetaData(Cursor groupMetaData) {
424         final KindSectionView groupKindSectionView =
425                 mKindSectionViewMap.get(GroupMembership.CONTENT_ITEM_TYPE);
426         if (groupKindSectionView == null) {
427             return;
428         }
429         groupKindSectionView.setGroupMetaData(groupMetaData);
430         if (mIsExpanded) {
431             groupKindSectionView.setHideWhenEmpty(false);
432             groupKindSectionView.updateEmptyEditors(/* shouldAnimate =*/ true);
433         }
434     }
435 
436     public void setIntentExtras(Bundle extras) {
437         mIntentExtras = extras;
438     }
439 
440     public void setState(RawContactDeltaList rawContactDeltas,
441             MaterialColorMapUtils.MaterialPalette materialPalette, ViewIdGenerator viewIdGenerator,
442             boolean hasNewContact, boolean isUserProfile, AccountWithDataSet primaryAccount,
443             long rawContactIdToDisplayAlone) {
444 
445         mRawContactDeltas = rawContactDeltas;
446         mRawContactIdToDisplayAlone = rawContactIdToDisplayAlone;
447 
448         mKindSectionViewMap.clear();
449         mKindSectionViews.removeAllViews();
450         mMoreFields.setVisibility(View.VISIBLE);
451 
452         mMaterialPalette = materialPalette;
453         mViewIdGenerator = viewIdGenerator;
454 
455         mHasNewContact = hasNewContact;
456         mIsUserProfile = isUserProfile;
457         mPrimaryAccount = primaryAccount;
458         if (mPrimaryAccount == null && mAccounts != null) {
459             mPrimaryAccount = ContactEditorUtils.create(getContext())
460                     .getOnlyOrDefaultAccount(AccountInfo.extractAccounts(mAccounts));
461         }
462         if (Log.isLoggable(TAG, Log.VERBOSE)) {
463             Log.v(TAG, "state: primary " + mPrimaryAccount);
464         }
465 
466         // Parse the given raw contact deltas
467         if (rawContactDeltas == null || rawContactDeltas.isEmpty()) {
468             elog("No raw contact deltas");
469             if (mListener != null) mListener.onBindEditorsFailed();
470             return;
471         }
472         pickRawContactDelta();
473         if (mCurrentRawContactDelta == null) {
474             elog("Couldn't pick a raw contact delta.");
475             if (mListener != null) mListener.onBindEditorsFailed();
476             return;
477         }
478         // Apply any intent extras now that we have selected a raw contact delta.
479         applyIntentExtras();
480         parseRawContactDelta();
481         if (mKindSectionDataMap.isEmpty()) {
482             elog("No kind section data parsed from RawContactDelta(s)");
483             if (mListener != null) mListener.onBindEditorsFailed();
484             return;
485         }
486 
487         final KindSectionData nameSectionData =
488                 mKindSectionDataMap.get(StructuredName.CONTENT_ITEM_TYPE);
489         // Ensure that a structured name and photo exists
490         if (nameSectionData != null) {
491             final RawContactDelta rawContactDelta =
492                     nameSectionData.getRawContactDelta();
493             RawContactModifier.ensureKindExists(
494                     rawContactDelta,
495                     rawContactDelta.getAccountType(mAccountTypeManager),
496                     StructuredName.CONTENT_ITEM_TYPE);
497             RawContactModifier.ensureKindExists(
498                     rawContactDelta,
499                     rawContactDelta.getAccountType(mAccountTypeManager),
500                     Photo.CONTENT_ITEM_TYPE);
501         }
502 
503         // Setup the view
504         addPhotoView();
505         setAccountInfo();
506         if (isReadOnlyRawContact()) {
507             // We're want to display the inputs fields for a single read only raw contact
508             addReadOnlyRawContactEditorViews();
509         } else {
510             setupEditorNormally();
511             // If we're inserting a new contact, request focus to bring up the keyboard for the
512             // name field.
513             if (mHasNewContact) {
514                 final StructuredNameEditorView name = getNameEditorView();
515                 if (name != null) {
516                     name.requestFocusForFirstEditField();
517                 }
518             }
519         }
520         if (mListener != null) mListener.onEditorsBound();
521     }
522 
523     public void setAccounts(List<AccountInfo> accounts) {
524         mAccounts.clear();
525         mAccounts.addAll(accounts);
526         // Update the account header
527         setAccountInfo();
528     }
529 
530     private void setupEditorNormally() {
531         addKindSectionViews();
532 
533         mMoreFields.setVisibility(hasMoreFields() ? View.VISIBLE : View.GONE);
534 
535         if (mIsExpanded) showAllFields();
536     }
537 
538     private boolean isReadOnlyRawContact() {
539         return !mCurrentRawContactDelta.getAccountType(mAccountTypeManager).areContactsWritable();
540     }
541 
542     private void pickRawContactDelta() {
543         if (Log.isLoggable(TAG, Log.VERBOSE)) {
544             Log.v(TAG, "parse: " + mRawContactDeltas.size() + " rawContactDelta(s)");
545         }
546         for (int j = 0; j < mRawContactDeltas.size(); j++) {
547             final RawContactDelta rawContactDelta = mRawContactDeltas.get(j);
548             if (Log.isLoggable(TAG, Log.VERBOSE)) {
549                 Log.v(TAG, "parse: " + j + " rawContactDelta" + rawContactDelta);
550             }
551             if (rawContactDelta == null || !rawContactDelta.isVisible()) continue;
552             final AccountType accountType = rawContactDelta.getAccountType(mAccountTypeManager);
553             if (accountType == null) continue;
554 
555             if (mRawContactIdToDisplayAlone > 0) {
556                 // Look for the raw contact if specified.
557                 if (rawContactDelta.getRawContactId().equals(mRawContactIdToDisplayAlone)) {
558                     mCurrentRawContactDelta = rawContactDelta;
559                     return;
560                 }
561             } else if (mPrimaryAccount != null
562                     && mPrimaryAccount.equals(rawContactDelta.getAccountWithDataSet())) {
563                 // Otherwise try to find the one that matches the default.
564                 mCurrentRawContactDelta = rawContactDelta;
565                 return;
566             } else if (accountType.areContactsWritable()){
567                 // TODO: Find better raw contact delta
568                 // Just select an arbitrary writable contact.
569                 mCurrentRawContactDelta = rawContactDelta;
570             }
571         }
572 
573     }
574 
575     private void applyIntentExtras() {
576         if (mIntentExtras == null || mIntentExtras.size() == 0) {
577             return;
578         }
579         final AccountTypeManager accountTypes = AccountTypeManager.getInstance(getContext());
580         final AccountType type = mCurrentRawContactDelta.getAccountType(accountTypes);
581 
582         RawContactModifier.parseExtras(getContext(), type, mCurrentRawContactDelta, mIntentExtras);
583         mIntentExtras = null;
584     }
585 
586     private void parseRawContactDelta() {
587         mKindSectionDataMap.clear();
588         mSortedMimetypes.clear();
589 
590         final AccountType accountType = mCurrentRawContactDelta.getAccountType(mAccountTypeManager);
591         final List<DataKind> dataKinds = accountType.getSortedDataKinds();
592         final int dataKindSize = dataKinds == null ? 0 : dataKinds.size();
593         if (Log.isLoggable(TAG, Log.VERBOSE)) {
594             Log.v(TAG, "parse: " + dataKindSize + " dataKinds(s)");
595         }
596 
597         for (int i = 0; i < dataKindSize; i++) {
598             final DataKind dataKind = dataKinds.get(i);
599             // Skip null and un-editable fields.
600             if (dataKind == null || !dataKind.editable) {
601                 if (Log.isLoggable(TAG, Log.VERBOSE)) {
602                     Log.v(TAG, "parse: " + i +
603                             (dataKind == null ? " dropped null data kind"
604                                     : " dropped uneditable mimetype: " + dataKind.mimeType));
605                 }
606                 continue;
607             }
608             final String mimeType = dataKind.mimeType;
609 
610             // Skip psuedo mime types
611             if (DataKind.PSEUDO_MIME_TYPE_NAME.equals(mimeType) ||
612                     DataKind.PSEUDO_MIME_TYPE_PHONETIC_NAME.equals(mimeType)) {
613                 if (Log.isLoggable(TAG, Log.VERBOSE)) {
614                     Log.v(TAG, "parse: " + i + " " + dataKind.mimeType + " dropped pseudo type");
615                 }
616                 continue;
617             }
618 
619             // Skip custom fields
620             // TODO: Handle them when we implement editing custom fields.
621             if (CustomDataItem.MIMETYPE_CUSTOM_FIELD.equals(mimeType)) {
622                 if (Log.isLoggable(TAG, Log.VERBOSE)) {
623                     Log.v(TAG, "parse: " + i + " " + dataKind.mimeType + " dropped custom field");
624                 }
625                 continue;
626             }
627 
628             final KindSectionData kindSectionData =
629                     new KindSectionData(accountType, dataKind, mCurrentRawContactDelta);
630             mKindSectionDataMap.put(mimeType, kindSectionData);
631             mSortedMimetypes.add(mimeType);
632 
633             if (Log.isLoggable(TAG, Log.VERBOSE)) {
634                 Log.v(TAG, "parse: " + i + " " + dataKind.mimeType + " " +
635                         kindSectionData.getValuesDeltas().size() + " value(s) " +
636                         kindSectionData.getNonEmptyValuesDeltas().size() + " non-empty value(s) " +
637                         kindSectionData.getVisibleValuesDeltas().size() +
638                         " visible value(s)");
639             }
640         }
641     }
642 
643     private void addReadOnlyRawContactEditorViews() {
644         mKindSectionViews.removeAllViews();
645         final AccountTypeManager accountTypes = AccountTypeManager.getInstance(
646                 getContext());
647         final AccountType type = mCurrentRawContactDelta.getAccountType(accountTypes);
648 
649         // Bail if invalid state or source
650         if (type == null) return;
651 
652         // Make sure we have StructuredName
653         RawContactModifier.ensureKindExists(
654                 mCurrentRawContactDelta, type, StructuredName.CONTENT_ITEM_TYPE);
655 
656         ValuesDelta primary;
657 
658         // Name
659         final Context context = getContext();
660         final Resources res = context.getResources();
661         primary = mCurrentRawContactDelta.getPrimaryEntry(StructuredName.CONTENT_ITEM_TYPE);
662         final String name = primary != null ? primary.getAsString(StructuredName.DISPLAY_NAME) :
663             getContext().getString(R.string.missing_name);
664         final Drawable nameDrawable = context.getDrawable(R.drawable.quantum_ic_person_vd_theme_24);
665         final String nameContentDescription = res.getString(R.string.header_name_entry);
666         bindData(nameDrawable, nameContentDescription, name, /* type */ null,
667                 /* isFirstEntry */ true);
668 
669         // Phones
670         final ArrayList<ValuesDelta> phones = mCurrentRawContactDelta
671                 .getMimeEntries(Phone.CONTENT_ITEM_TYPE);
672         final Drawable phoneDrawable = context.getDrawable(R.drawable.quantum_ic_phone_vd_theme_24);
673         final String phoneContentDescription = res.getString(R.string.header_phone_entry);
674         if (phones != null) {
675             boolean isFirstPhoneBound = true;
676             for (ValuesDelta phone : phones) {
677                 final String phoneNumber = phone.getPhoneNumber();
678                 if (TextUtils.isEmpty(phoneNumber)) {
679                     continue;
680                 }
681                 final String formattedNumber = PhoneNumberUtilsCompat.formatNumber(
682                         phoneNumber, phone.getPhoneNormalizedNumber(),
683                         GeoUtil.getCurrentCountryIso(getContext()));
684                 CharSequence phoneType = null;
685                 if (phone.hasPhoneType()) {
686                     phoneType = Phone.getTypeLabel(
687                             res, phone.getPhoneType(), phone.getPhoneLabel());
688                 }
689                 bindData(phoneDrawable, phoneContentDescription, formattedNumber, phoneType,
690                         isFirstPhoneBound, true);
691                 isFirstPhoneBound = false;
692             }
693         }
694 
695         // Emails
696         final ArrayList<ValuesDelta> emails = mCurrentRawContactDelta
697                 .getMimeEntries(Email.CONTENT_ITEM_TYPE);
698         final Drawable emailDrawable = context.getDrawable(R.drawable.quantum_ic_email_vd_theme_24);
699         final String emailContentDescription = res.getString(R.string.header_email_entry);
700         if (emails != null) {
701             boolean isFirstEmailBound = true;
702             for (ValuesDelta email : emails) {
703                 final String emailAddress = email.getEmailData();
704                 if (TextUtils.isEmpty(emailAddress)) {
705                     continue;
706                 }
707                 CharSequence emailType = null;
708                 if (email.hasEmailType()) {
709                     emailType = Email.getTypeLabel(
710                             res, email.getEmailType(), email.getEmailLabel());
711                 }
712                 bindData(emailDrawable, emailContentDescription, emailAddress, emailType,
713                         isFirstEmailBound);
714                 isFirstEmailBound = false;
715             }
716         }
717 
718         mKindSectionViews.setVisibility(mKindSectionViews.getChildCount() > 0 ? VISIBLE : GONE);
719         // Hide the "More fields" link
720         mMoreFields.setVisibility(GONE);
721     }
722 
723     private void bindData(Drawable icon, String iconContentDescription, CharSequence data,
724             CharSequence type, boolean isFirstEntry) {
725         bindData(icon, iconContentDescription, data, type, isFirstEntry, false);
726     }
727 
728     private void bindData(Drawable icon, String iconContentDescription, CharSequence data,
729             CharSequence type, boolean isFirstEntry, boolean forceLTR) {
730         final View field = mLayoutInflater.inflate(R.layout.item_read_only_field, mKindSectionViews,
731                 /* attachToRoot */ false);
732         if (isFirstEntry) {
733             final ImageView imageView = (ImageView) field.findViewById(R.id.kind_icon);
734             imageView.setImageDrawable(icon);
735             imageView.setContentDescription(iconContentDescription);
736         } else {
737             final ImageView imageView = (ImageView) field.findViewById(R.id.kind_icon);
738             imageView.setVisibility(View.INVISIBLE);
739             imageView.setContentDescription(null);
740         }
741         final TextView dataView = (TextView) field.findViewById(R.id.data);
742         dataView.setText(data);
743         if (forceLTR) {
744             dataView.setTextDirection(View.TEXT_DIRECTION_LTR);
745         }
746         final TextView typeView = (TextView) field.findViewById(R.id.type);
747         if (!TextUtils.isEmpty(type)) {
748             typeView.setText(type);
749         } else {
750             typeView.setVisibility(View.GONE);
751         }
752         mKindSectionViews.addView(field);
753     }
754 
755     private void setAccountInfo() {
756         if (mCurrentRawContactDelta == null && mPrimaryAccount == null) {
757             return;
758         }
759         final AccountTypeManager accountTypeManager = AccountTypeManager.getInstance(getContext());
760         final AccountInfo account = mCurrentRawContactDelta != null
761                 ? accountTypeManager.getAccountInfoForAccount(
762                 mCurrentRawContactDelta.getAccountWithDataSet())
763                 : accountTypeManager.getAccountInfoForAccount(mPrimaryAccount);
764 
765         // Accounts haven't loaded yet or we are editing.
766         if (mAccounts.isEmpty()) {
767             mAccounts.add(account);
768         }
769 
770         // Get the account information for the primary raw contact delta
771         if (isReadOnlyRawContact()) {
772             final String accountType = account.getTypeLabel().toString();
773             setAccountHeader(accountType,
774                     getResources().getString(
775                             R.string.editor_account_selector_read_only_title, accountType));
776         } else {
777             final String accountLabel = mIsUserProfile
778                     ? EditorUiUtils.getAccountHeaderLabelForMyProfile(getContext(), account)
779                     : account.getNameLabel().toString();
780             setAccountHeader(getResources().getString(R.string.editor_account_selector_title),
781                     accountLabel);
782         }
783 
784         // If we're saving a new contact and there are multiple accounts, add the account selector.
785         if (mHasNewContact && !mIsUserProfile && mAccounts.size() > 1) {
786             addAccountSelector(mCurrentRawContactDelta);
787         }
788     }
789 
790     private void setAccountHeader(String primaryText, String secondaryText) {
791         mAccountHeaderPrimaryText.setText(primaryText);
792         mAccountHeaderSecondaryText.setText(secondaryText);
793 
794         // Set the icon
795         final AccountType accountType =
796                 mCurrentRawContactDelta.getRawContactAccountType(getContext());
797         mAccountHeaderIcon.setImageDrawable(accountType.getDisplayIcon(getContext()));
798 
799         // Set the content description
800         mAccountHeaderContainer.setContentDescription(
801                 EditorUiUtils.getAccountInfoContentDescription(secondaryText, primaryText));
802     }
803 
804     private void addAccountSelector(final RawContactDelta rawContactDelta) {
805         // Add handlers for choosing another account to save to.
806         mAccountHeaderExpanderIcon.setVisibility(View.VISIBLE);
807         final OnClickListener clickListener = new OnClickListener() {
808             @Override
809             public void onClick(View v) {
810                 final AccountWithDataSet current = rawContactDelta.getAccountWithDataSet();
811                 AccountInfo.sortAccounts(current, mAccounts);
812                 final ListPopupWindow popup = new ListPopupWindow(getContext(), null);
813                 final AccountsListAdapter adapter =
814                         new AccountsListAdapter(getContext(), mAccounts, current);
815                 popup.setWidth(mAccountHeaderContainer.getWidth());
816                 popup.setAnchorView(mAccountHeaderContainer);
817                 popup.setAdapter(adapter);
818                 popup.setModal(true);
819                 popup.setInputMethodMode(ListPopupWindow.INPUT_METHOD_NOT_NEEDED);
820                 popup.setOnItemClickListener(new AdapterView.OnItemClickListener() {
821                     @Override
822                     public void onItemClick(AdapterView<?> parent, View view, int position,
823                             long id) {
824                         UiClosables.closeQuietly(popup);
825                         final AccountWithDataSet newAccount = adapter.getItem(position);
826                         if (mListener != null && !mPrimaryAccount.equals(newAccount)) {
827                             mIsExpanded = false;
828                             mListener.onRebindEditorsForNewContact(
829                                     rawContactDelta,
830                                     mPrimaryAccount,
831                                     newAccount);
832                         }
833                     }
834                 });
835                 popup.show();
836             }
837         };
838         mAccountHeaderContainer.setOnClickListener(clickListener);
839         // Make the expander icon clickable so that it will be announced as a button by
840         // talkback
841         mAccountHeaderExpanderIcon.setOnClickListener(clickListener);
842     }
843 
844     private void addPhotoView() {
845         if (!mCurrentRawContactDelta.hasMimeEntries(Photo.CONTENT_ITEM_TYPE)) {
846             wlog("No photo mimetype for this raw contact.");
847             mPhotoView.setVisibility(GONE);
848             return;
849         } else {
850             mPhotoView.setVisibility(VISIBLE);
851         }
852 
853         final ValuesDelta superPrimaryDelta = mCurrentRawContactDelta
854                 .getSuperPrimaryEntry(Photo.CONTENT_ITEM_TYPE);
855         if (superPrimaryDelta == null) {
856             Log.wtf(TAG, "addPhotoView: no ValueDelta found for current RawContactDelta"
857                     + "that supports a photo.");
858             mPhotoView.setVisibility(GONE);
859             return;
860         }
861         // Set the photo view
862         mPhotoView.setPalette(mMaterialPalette);
863         mPhotoView.setPhoto(superPrimaryDelta);
864 
865         if (isReadOnlyRawContact()) {
866             mPhotoView.setReadOnly(true);
867             return;
868         }
869         mPhotoView.setReadOnly(false);
870         mPhotoValuesDelta = superPrimaryDelta;
871     }
872 
873     private void addKindSectionViews() {
874         int i = -1;
875 
876         for (String mimeType : mSortedMimetypes) {
877             i++;
878             // Ignore mime types that we've already handled
879             if (Photo.CONTENT_ITEM_TYPE.equals(mimeType)) {
880                 if (Log.isLoggable(TAG, Log.VERBOSE)) {
881                     Log.v(TAG, "kind: " + i + " " + mimeType + " dropped");
882                 }
883                 continue;
884             }
885             final KindSectionView kindSectionView;
886             final KindSectionData kindSectionData = mKindSectionDataMap.get(mimeType);
887             kindSectionView = inflateKindSectionView(mKindSectionViews, kindSectionData, mimeType);
888             mKindSectionViews.addView(kindSectionView);
889 
890             // Keep a pointer to the KindSectionView for each mimeType
891             mKindSectionViewMap.put(mimeType, kindSectionView);
892         }
893     }
894 
895     private KindSectionView inflateKindSectionView(ViewGroup viewGroup,
896             KindSectionData kindSectionData, String mimeType) {
897         final KindSectionView kindSectionView = (KindSectionView)
898                 mLayoutInflater.inflate(R.layout.item_kind_section, viewGroup,
899                         /* attachToRoot =*/ false);
900         kindSectionView.setIsUserProfile(mIsUserProfile);
901 
902         if (Phone.CONTENT_ITEM_TYPE.equals(mimeType)
903                 || Email.CONTENT_ITEM_TYPE.equals(mimeType)) {
904             // Phone numbers and email addresses are always displayed,
905             // even if they are empty
906             kindSectionView.setHideWhenEmpty(false);
907         }
908 
909         // Since phone numbers and email addresses displayed even if they are empty,
910         // they will be the only types you add new values to initially for new contacts
911         kindSectionView.setShowOneEmptyEditor(true);
912 
913         kindSectionView.setState(kindSectionData, mViewIdGenerator, mListener);
914 
915         return kindSectionView;
916     }
917 
918     private void showAllFields() {
919         // Stop hiding empty editors and allow the user to enter values for all kinds now
920         for (int i = 0; i < mKindSectionViews.getChildCount(); i++) {
921             final KindSectionView kindSectionView =
922                     (KindSectionView) mKindSectionViews.getChildAt(i);
923             kindSectionView.setHideWhenEmpty(false);
924             kindSectionView.updateEmptyEditors(/* shouldAnimate =*/ true);
925         }
926         mIsExpanded = true;
927 
928         // Hide the more fields button
929         mMoreFields.setVisibility(View.GONE);
930     }
931 
932     private boolean hasMoreFields() {
933         for (KindSectionView section : mKindSectionViewMap.values()) {
934             if (section.getVisibility() != View.VISIBLE) {
935                 return true;
936             }
937         }
938         return false;
939     }
940 
941     private static void wlog(String message) {
942         if (Log.isLoggable(TAG, Log.WARN)) {
943             Log.w(TAG, message);
944         }
945     }
946 
947     private static void elog(String message) {
948         Log.e(TAG, message);
949     }
950 }
951