RawContactEditorView.java revision ac5bd644bcd2c27294a70774abfa9e24fb3d5c52
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
17package com.android.contacts.editor;
18
19import android.content.Context;
20import android.content.res.Resources;
21import android.database.Cursor;
22import android.graphics.drawable.Drawable;
23import android.net.Uri;
24import android.os.Parcel;
25import android.os.Parcelable;
26import android.provider.ContactsContract.CommonDataKinds.Email;
27import android.provider.ContactsContract.CommonDataKinds.Event;
28import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
29import android.provider.ContactsContract.CommonDataKinds.Im;
30import android.provider.ContactsContract.CommonDataKinds.Nickname;
31import android.provider.ContactsContract.CommonDataKinds.Note;
32import android.provider.ContactsContract.CommonDataKinds.Organization;
33import android.provider.ContactsContract.CommonDataKinds.Phone;
34import android.provider.ContactsContract.CommonDataKinds.Photo;
35import android.provider.ContactsContract.CommonDataKinds.Relation;
36import android.provider.ContactsContract.CommonDataKinds.SipAddress;
37import android.provider.ContactsContract.CommonDataKinds.StructuredName;
38import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
39import android.provider.ContactsContract.CommonDataKinds.Website;
40import android.text.TextUtils;
41import android.util.AttributeSet;
42import android.util.Log;
43import android.view.LayoutInflater;
44import android.view.View;
45import android.view.ViewGroup;
46import android.widget.AdapterView;
47import android.widget.ImageView;
48import android.widget.LinearLayout;
49import android.widget.ListPopupWindow;
50import android.widget.TextView;
51
52import com.android.contacts.R;
53import com.android.contacts.common.GeoUtil;
54import com.android.contacts.common.compat.PhoneNumberUtilsCompat;
55import com.android.contacts.common.model.AccountTypeManager;
56import com.android.contacts.common.model.RawContactDelta;
57import com.android.contacts.common.model.RawContactDeltaList;
58import com.android.contacts.common.model.RawContactModifier;
59import com.android.contacts.common.model.ValuesDelta;
60import com.android.contacts.common.model.account.AccountDisplayInfo;
61import com.android.contacts.common.model.account.AccountDisplayInfoFactory;
62import com.android.contacts.common.model.account.AccountType;
63import com.android.contacts.common.model.account.AccountWithDataSet;
64import com.android.contacts.common.model.dataitem.CustomDataItem;
65import com.android.contacts.common.model.dataitem.DataKind;
66import com.android.contacts.common.util.AccountsListAdapter;
67import com.android.contacts.common.util.MaterialColorMapUtils;
68import com.android.contacts.util.UiClosables;
69
70import java.io.FileNotFoundException;
71import java.util.ArrayList;
72import java.util.Arrays;
73import java.util.Comparator;
74import java.util.HashMap;
75import java.util.List;
76import java.util.Map;
77import java.util.Set;
78import java.util.TreeSet;
79
80/**
81 * View to display information from multiple {@link RawContactDelta}s grouped together.
82 */
83public 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 AccountDisplayInfoFactory mAccountDisplayInfoFactory;
199    private LayoutInflater mLayoutInflater;
200
201    private ViewIdGenerator mViewIdGenerator;
202    private MaterialColorMapUtils.MaterialPalette mMaterialPalette;
203    private boolean mHasNewContact;
204    private boolean mIsUserProfile;
205    private AccountWithDataSet mPrimaryAccount;
206    private RawContactDeltaList mRawContactDeltas;
207    private RawContactDelta mCurrentRawContactDelta;
208    private long mRawContactIdToDisplayAlone = -1;
209    private boolean mIsEditingReadOnlyRawContactWithNewContact;
210    private Map<String, KindSectionData> mKindSectionDataMap = new HashMap<>();
211    private Set<String> mSortedMimetypes = new TreeSet<>(new MimeTypeComparator());
212
213    // Account header
214    private View mAccountHeaderContainer;
215    private TextView mAccountHeaderType;
216    private TextView mAccountHeaderName;
217    private ImageView mAccountHeaderIcon;
218    private ImageView mAccountHeaderExpanderIcon;
219
220    private PhotoEditorView mPhotoView;
221    private ViewGroup mKindSectionViews;
222    private Map<String, KindSectionView> mKindSectionViewMap = new HashMap<>();
223    private View mMoreFields;
224
225    private boolean mIsExpanded;
226
227    private ValuesDelta mPhotoValuesDelta;
228
229    public RawContactEditorView(Context context) {
230        super(context);
231    }
232
233    public RawContactEditorView(Context context, AttributeSet attrs) {
234        super(context, attrs);
235    }
236
237    /**
238     * Sets the receiver for {@link RawContactEditorView} callbacks.
239     */
240    public void setListener(Listener listener) {
241        mListener = listener;
242    }
243
244    @Override
245    protected void onFinishInflate() {
246        super.onFinishInflate();
247
248        mAccountTypeManager = AccountTypeManager.getInstance(getContext());
249        mAccountDisplayInfoFactory = AccountDisplayInfoFactory.forWritableAccounts(getContext());
250        mLayoutInflater = (LayoutInflater)
251                getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
252
253        // Account header
254        mAccountHeaderContainer = findViewById(R.id.account_header_container);
255        mAccountHeaderType = (TextView) findViewById(R.id.account_type);
256        mAccountHeaderName = (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.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 setState(RawContactDeltaList rawContactDeltas,
430            MaterialColorMapUtils.MaterialPalette materialPalette, ViewIdGenerator viewIdGenerator,
431            boolean hasNewContact, boolean isUserProfile, AccountWithDataSet primaryAccount,
432            long rawContactIdToDisplayAlone, boolean isEditingReadOnlyRawContactWithNewContact) {
433
434        mRawContactDeltas = rawContactDeltas;
435        mRawContactIdToDisplayAlone = rawContactIdToDisplayAlone;
436        mIsEditingReadOnlyRawContactWithNewContact = isEditingReadOnlyRawContactWithNewContact;
437
438        mKindSectionViewMap.clear();
439        mKindSectionViews.removeAllViews();
440        mMoreFields.setVisibility(View.VISIBLE);
441
442        mMaterialPalette = materialPalette;
443        mViewIdGenerator = viewIdGenerator;
444
445        mHasNewContact = hasNewContact;
446        mIsUserProfile = isUserProfile;
447        mPrimaryAccount = primaryAccount;
448        if (mPrimaryAccount == null) {
449            mPrimaryAccount = ContactEditorUtils.create(getContext()).getOnlyOrDefaultAccount();
450        }
451        vlog("state: primary " + mPrimaryAccount);
452
453        // Parse the given raw contact deltas
454        if (rawContactDeltas == null || rawContactDeltas.isEmpty()) {
455            elog("No raw contact deltas");
456            if (mListener != null) mListener.onBindEditorsFailed();
457            return;
458        }
459        pickRawContactDelta();
460        parseRawContactDelta();
461        if (mKindSectionDataMap.isEmpty()) {
462            elog("No kind section data parsed from RawContactDelta(s)");
463            if (mListener != null) mListener.onBindEditorsFailed();
464            return;
465        }
466
467        final KindSectionData nameSectionData =
468                mKindSectionDataMap.get(StructuredName.CONTENT_ITEM_TYPE);
469        // Ensure that a structured name and photo exists
470        if (nameSectionData != null) {
471            final RawContactDelta rawContactDelta =
472                    nameSectionData.getRawContactDelta();
473            RawContactModifier.ensureKindExists(
474                    rawContactDelta,
475                    rawContactDelta.getAccountType(mAccountTypeManager),
476                    StructuredName.CONTENT_ITEM_TYPE);
477            RawContactModifier.ensureKindExists(
478                    rawContactDelta,
479                    rawContactDelta.getAccountType(mAccountTypeManager),
480                    Photo.CONTENT_ITEM_TYPE);
481        }
482
483        // Setup the view
484        addPhotoView();
485        if (isReadOnlyRawContact()) {
486            // We're want to display the inputs fields for a single read only raw contact
487            addReadOnlyRawContactEditorViews();
488        } else {
489            setupEditorNormally();
490        }
491        if (mListener != null) mListener.onEditorsBound();
492    }
493
494    private void setupEditorNormally() {
495        addAccountInfo();
496        addKindSectionViews();
497
498        mMoreFields.setVisibility(hasMoreFields() ? View.VISIBLE : View.GONE);
499
500        if (mIsExpanded) showAllFields();
501    }
502
503    private boolean isReadOnlyRawContact() {
504        return !mCurrentRawContactDelta.getAccountType(mAccountTypeManager).areContactsWritable();
505    }
506
507    private void pickRawContactDelta() {
508        vlog("parse: " + mRawContactDeltas.size() + " rawContactDelta(s)");
509        for (int j = 0; j < mRawContactDeltas.size(); j++) {
510            final RawContactDelta rawContactDelta = mRawContactDeltas.get(j);
511            vlog("parse: " + j + " rawContactDelta" + rawContactDelta);
512            if (rawContactDelta == null || !rawContactDelta.isVisible()) continue;
513            final AccountType accountType = rawContactDelta.getAccountType(mAccountTypeManager);
514            if (accountType == null) continue;
515
516            if (mRawContactIdToDisplayAlone > 0) {
517                // Look for the raw contact if specified.
518                if (rawContactDelta.getRawContactId().equals(mRawContactIdToDisplayAlone)) {
519                    mCurrentRawContactDelta = rawContactDelta;
520                    return;
521                }
522            } else if (mPrimaryAccount != null
523                    && mPrimaryAccount.equals(rawContactDelta.getAccountWithDataSet())) {
524                // Otherwise try to find the one that matches the default.
525                mCurrentRawContactDelta = rawContactDelta;
526                return;
527            } else if (accountType.areContactsWritable()){
528                // TODO: Find better raw contact delta
529                // Just select an arbitrary writable contact.
530                mCurrentRawContactDelta = rawContactDelta;
531            }
532        }
533
534    }
535
536    private void parseRawContactDelta() {
537        mKindSectionDataMap.clear();
538        mSortedMimetypes.clear();
539
540        final AccountType accountType = mCurrentRawContactDelta.getAccountType(mAccountTypeManager);
541        final List<DataKind> dataKinds = accountType.getSortedDataKinds();
542        final int dataKindSize = dataKinds == null ? 0 : dataKinds.size();
543        vlog("parse: " + dataKindSize + " dataKinds(s)");
544
545        for (int i = 0; i < dataKindSize; i++) {
546            final DataKind dataKind = dataKinds.get(i);
547            // Skip null and un-editable fields.
548            if (dataKind == null || !dataKind.editable) {
549                vlog("parse: " + i +
550                        (dataKind == null ? " dropped null data kind"
551                        : " dropped uneditable mimetype: " + dataKind.mimeType));
552                continue;
553            }
554            final String mimeType = dataKind.mimeType;
555
556            // Skip psuedo mime types
557            if (DataKind.PSEUDO_MIME_TYPE_NAME.equals(mimeType) ||
558                    DataKind.PSEUDO_MIME_TYPE_PHONETIC_NAME.equals(mimeType)) {
559                vlog("parse: " + i + " " + dataKind.mimeType + " dropped pseudo type");
560                continue;
561            }
562
563            // Skip custom fields
564            // TODO: Handle them when we implement editing custom fields.
565            if (CustomDataItem.MIMETYPE_CUSTOM_FIELD.equals(mimeType)) {
566                vlog("parse: " + i + " " + dataKind.mimeType + " dropped custom field");
567                continue;
568            }
569
570            final KindSectionData kindSectionData =
571                    new KindSectionData(accountType, dataKind, mCurrentRawContactDelta);
572            mKindSectionDataMap.put(mimeType, kindSectionData);
573            mSortedMimetypes.add(mimeType);
574
575            vlog("parse: " + i + " " + dataKind.mimeType + " " +
576                    kindSectionData.getValuesDeltas().size() + " value(s) " +
577                    kindSectionData.getNonEmptyValuesDeltas().size() + " non-empty value(s) " +
578                    kindSectionData.getVisibleValuesDeltas().size() +
579                    " visible value(s)");
580        }
581    }
582
583    private void addReadOnlyRawContactEditorViews() {
584        mKindSectionViews.removeAllViews();
585        addAccountInfo();
586        final AccountTypeManager accountTypes = AccountTypeManager.getInstance(
587                getContext());
588        final AccountType type = mCurrentRawContactDelta.getAccountType(accountTypes);
589
590        // Bail if invalid state or source
591        if (type == null) return;
592
593        // Make sure we have StructuredName
594        RawContactModifier.ensureKindExists(
595                mCurrentRawContactDelta, type, StructuredName.CONTENT_ITEM_TYPE);
596
597        ValuesDelta primary;
598
599        // Name
600        final Context context = getContext();
601        final Resources res = context.getResources();
602        primary = mCurrentRawContactDelta.getPrimaryEntry(StructuredName.CONTENT_ITEM_TYPE);
603        final String name = primary != null ? primary.getAsString(StructuredName.DISPLAY_NAME) :
604            getContext().getString(R.string.missing_name);
605        final Drawable nameDrawable = context.getDrawable(R.drawable.ic_person_24dp);
606        final String nameContentDescription = res.getString(R.string.header_name_entry);
607        bindData(nameDrawable, nameContentDescription, name, /* type */ null,
608                /* isFirstEntry */ true);
609
610        // Phones
611        final ArrayList<ValuesDelta> phones = mCurrentRawContactDelta
612                .getMimeEntries(Phone.CONTENT_ITEM_TYPE);
613        final Drawable phoneDrawable = context.getDrawable(R.drawable.ic_phone_24dp);
614        final String phoneContentDescription = res.getString(R.string.header_phone_entry);
615        if (phones != null) {
616            boolean isFirstPhoneBound = true;
617            for (ValuesDelta phone : phones) {
618                final String phoneNumber = phone.getPhoneNumber();
619                if (TextUtils.isEmpty(phoneNumber)) {
620                    continue;
621                }
622                final String formattedNumber = PhoneNumberUtilsCompat.formatNumber(
623                        phoneNumber, phone.getPhoneNormalizedNumber(),
624                        GeoUtil.getCurrentCountryIso(getContext()));
625                CharSequence phoneType = null;
626                if (phone.hasPhoneType()) {
627                    phoneType = Phone.getTypeLabel(
628                            res, phone.getPhoneType(), phone.getPhoneLabel());
629                }
630                bindData(phoneDrawable, phoneContentDescription, formattedNumber, phoneType,
631                        isFirstPhoneBound, true);
632                isFirstPhoneBound = false;
633            }
634        }
635
636        // Emails
637        final ArrayList<ValuesDelta> emails = mCurrentRawContactDelta
638                .getMimeEntries(Email.CONTENT_ITEM_TYPE);
639        final Drawable emailDrawable = context.getDrawable(R.drawable.ic_email_24dp);
640        final String emailContentDescription = res.getString(R.string.header_email_entry);
641        if (emails != null) {
642            boolean isFirstEmailBound = true;
643            for (ValuesDelta email : emails) {
644                final String emailAddress = email.getEmailData();
645                if (TextUtils.isEmpty(emailAddress)) {
646                    continue;
647                }
648                CharSequence emailType = null;
649                if (email.hasEmailType()) {
650                    emailType = Email.getTypeLabel(
651                            res, email.getEmailType(), email.getEmailLabel());
652                }
653                bindData(emailDrawable, emailContentDescription, emailAddress, emailType,
654                        isFirstEmailBound);
655                isFirstEmailBound = false;
656            }
657        }
658
659        mKindSectionViews.setVisibility(mKindSectionViews.getChildCount() > 0 ? VISIBLE : GONE);
660        // Hide the "More fields" link
661        mMoreFields.setVisibility(GONE);
662    }
663
664    private void bindData(Drawable icon, String iconContentDescription, CharSequence data,
665            CharSequence type, boolean isFirstEntry) {
666        bindData(icon, iconContentDescription, data, type, isFirstEntry, false);
667    }
668
669    private void bindData(Drawable icon, String iconContentDescription, CharSequence data,
670            CharSequence type, boolean isFirstEntry, boolean forceLTR) {
671        final LayoutInflater inflater = (LayoutInflater) getContext().getSystemService(
672                Context.LAYOUT_INFLATER_SERVICE);
673        final View field = inflater.inflate(R.layout.item_read_only_field, mKindSectionViews,
674                false);
675        if (isFirstEntry) {
676            final ImageView imageView = (ImageView) field.findViewById(R.id.kind_icon);
677            imageView.setImageDrawable(icon);
678            imageView.setContentDescription(iconContentDescription);
679        } else {
680            final ImageView imageView = (ImageView) field.findViewById(R.id.kind_icon);
681            imageView.setVisibility(View.INVISIBLE);
682            imageView.setContentDescription(null);
683        }
684        final TextView dataView = (TextView) field.findViewById(R.id.data);
685        dataView.setText(data);
686        if (forceLTR) {
687            dataView.setTextDirection(View.TEXT_DIRECTION_LTR);
688        }
689        final TextView typeView = (TextView) field.findViewById(R.id.type);
690        if (!TextUtils.isEmpty(type)) {
691            typeView.setText(type);
692        } else {
693            typeView.setVisibility(View.GONE);
694        }
695        mKindSectionViews.addView(field);
696    }
697
698    private void addAccountInfo() {
699        mAccountHeaderContainer.setVisibility(View.GONE);
700
701        final AccountDisplayInfo account =
702                mAccountDisplayInfoFactory.getAccountDisplayInfoFor(mCurrentRawContactDelta);
703
704        // Get the account information for the primary raw contact delta
705        final String accountLabel = mIsUserProfile
706                ? EditorUiUtils.getAccountHeaderLabelForMyProfile(getContext(), account)
707                : account.getNameLabel().toString();
708
709        addAccountHeader(accountLabel);
710
711        // If we're saving a new contact and there are multiple accounts, add the account selector.
712        final List<AccountWithDataSet> accounts =
713                AccountTypeManager.getInstance(getContext()).getAccounts(true);
714        if (mHasNewContact && !mIsUserProfile && accounts.size() > 1) {
715            addAccountSelector(mCurrentRawContactDelta);
716        }
717    }
718
719    private void addAccountHeader(String accountLabel) {
720        mAccountHeaderContainer.setVisibility(View.VISIBLE);
721
722        // Set the account name
723        mAccountHeaderName.setVisibility(View.VISIBLE);
724        mAccountHeaderName.setText(accountLabel);
725
726        // Set the account type
727        final String selectorTitle = getResources().getString(isReadOnlyRawContact() ?
728                R.string.editor_account_selector_read_only_title :
729                R.string.editor_account_selector_title);
730        mAccountHeaderType.setText(selectorTitle);
731
732        // Set the icon
733        final AccountType accountType =
734                mCurrentRawContactDelta.getRawContactAccountType(getContext());
735        mAccountHeaderIcon.setImageDrawable(accountType.getDisplayIcon(getContext()));
736
737        // Set the content description
738        mAccountHeaderContainer.setContentDescription(
739                EditorUiUtils.getAccountInfoContentDescription(accountLabel,
740                        selectorTitle));
741    }
742
743    private void addAccountSelector(final RawContactDelta rawContactDelta) {
744        // Add handlers for choosing another account to save to.
745        mAccountHeaderExpanderIcon.setVisibility(View.VISIBLE);
746        mAccountHeaderContainer.setOnClickListener(new View.OnClickListener() {
747            @Override
748            public void onClick(View v) {
749                final ListPopupWindow popup = new ListPopupWindow(getContext(), null);
750                final AccountsListAdapter adapter =
751                        new AccountsListAdapter(getContext(),
752                                AccountsListAdapter.AccountListFilter.ACCOUNTS_CONTACT_WRITABLE,
753                                mPrimaryAccount);
754                popup.setWidth(mAccountHeaderContainer.getWidth());
755                popup.setAnchorView(mAccountHeaderContainer);
756                popup.setAdapter(adapter);
757                popup.setModal(true);
758                popup.setInputMethodMode(ListPopupWindow.INPUT_METHOD_NOT_NEEDED);
759                popup.setOnItemClickListener(new AdapterView.OnItemClickListener() {
760                    @Override
761                    public void onItemClick(AdapterView<?> parent, View view, int position,
762                            long id) {
763                        UiClosables.closeQuietly(popup);
764                        final AccountWithDataSet newAccount = adapter.getItem(position);
765                        if (mListener != null && !mPrimaryAccount.equals(newAccount)) {
766                            mIsExpanded = false;
767                            mListener.onRebindEditorsForNewContact(
768                                    rawContactDelta,
769                                    mPrimaryAccount,
770                                    newAccount);
771                        }
772                    }
773                });
774                popup.show();
775            }
776        });
777    }
778
779    private void addPhotoView() {
780        if (!mCurrentRawContactDelta.hasMimeEntries(Photo.CONTENT_ITEM_TYPE)) {
781            wlog("No photo mimetype for this raw contact.");
782            mPhotoView.setVisibility(GONE);
783            return;
784        } else {
785            mPhotoView.setVisibility(VISIBLE);
786        }
787
788        final ValuesDelta superPrimaryDelta = mCurrentRawContactDelta
789                .getSuperPrimaryEntry(Photo.CONTENT_ITEM_TYPE);
790        if (superPrimaryDelta == null) {
791            Log.wtf(TAG, "addPhotoView: no ValueDelta found for current RawContactDelta"
792                    + "that supports a photo.");
793            mPhotoView.setVisibility(GONE);
794            return;
795        }
796        // Set the photo view
797        mPhotoView.setPalette(mMaterialPalette);
798        mPhotoView.setPhoto(superPrimaryDelta);
799
800        if (isReadOnlyRawContact()) {
801            mPhotoView.setReadOnly(true);
802            return;
803        }
804        mPhotoView.setReadOnly(false);
805        mPhotoValuesDelta = superPrimaryDelta;
806    }
807
808    private void addKindSectionViews() {
809        int i = -1;
810
811        for (String mimeType : mSortedMimetypes) {
812            i++;
813            // Ignore mime types that we've already handled
814            if (Photo.CONTENT_ITEM_TYPE.equals(mimeType)) {
815                vlog("kind: " + i + " " + mimeType + " dropped");
816                continue;
817            }
818            final KindSectionView kindSectionView;
819            final KindSectionData kindSectionData = mKindSectionDataMap.get(mimeType);
820            kindSectionView = inflateKindSectionView(mKindSectionViews, kindSectionData, mimeType);
821            mKindSectionViews.addView(kindSectionView);
822
823            // Keep a pointer to the KindSectionView for each mimeType
824            mKindSectionViewMap.put(mimeType, kindSectionView);
825        }
826    }
827
828    private KindSectionView inflateKindSectionView(ViewGroup viewGroup,
829            KindSectionData kindSectionData, String mimeType) {
830        final KindSectionView kindSectionView = (KindSectionView)
831                mLayoutInflater.inflate(R.layout.item_kind_section, viewGroup,
832                        /* attachToRoot =*/ false);
833        kindSectionView.setIsUserProfile(mIsUserProfile);
834
835        if (Phone.CONTENT_ITEM_TYPE.equals(mimeType)
836                || Email.CONTENT_ITEM_TYPE.equals(mimeType)) {
837            // Phone numbers and email addresses are always displayed,
838            // even if they are empty
839            kindSectionView.setHideWhenEmpty(false);
840        }
841
842        // Since phone numbers and email addresses displayed even if they are empty,
843        // they will be the only types you add new values to initially for new contacts
844        kindSectionView.setShowOneEmptyEditor(true);
845
846        kindSectionView.setState(kindSectionData, mViewIdGenerator, mListener);
847
848        return kindSectionView;
849    }
850
851    private void showAllFields() {
852        // Stop hiding empty editors and allow the user to enter values for all kinds now
853        for (int i = 0; i < mKindSectionViews.getChildCount(); i++) {
854            final KindSectionView kindSectionView =
855                    (KindSectionView) mKindSectionViews.getChildAt(i);
856            kindSectionView.setHideWhenEmpty(false);
857            kindSectionView.updateEmptyEditors(/* shouldAnimate =*/ true);
858        }
859        mIsExpanded = true;
860
861        // Hide the more fields button
862        mMoreFields.setVisibility(View.GONE);
863    }
864
865    private boolean hasMoreFields() {
866        for (KindSectionView section : mKindSectionViewMap.values()) {
867            if (section.getVisibility() != View.VISIBLE) {
868                return true;
869            }
870        }
871        return false;
872    }
873
874    private static void vlog(String message) {
875        if (Log.isLoggable(TAG, Log.VERBOSE)) {
876            Log.v(TAG, message);
877        }
878    }
879
880    private static void wlog(String message) {
881        if (Log.isLoggable(TAG, Log.WARN)) {
882            Log.w(TAG, message);
883        }
884    }
885
886    private static void elog(String message) {
887        Log.e(TAG, message);
888    }
889}
890