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