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