1 /*
2  * Copyright (C) 2015 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.contacts.editor;
18 
19 import android.content.Context;
20 import android.content.res.Resources;
21 import android.database.Cursor;
22 import android.graphics.drawable.Drawable;
23 import android.net.Uri;
24 import android.os.Bundle;
25 import android.os.Parcel;
26 import android.os.Parcelable;
27 import android.provider.ContactsContract.CommonDataKinds.Email;
28 import android.provider.ContactsContract.CommonDataKinds.Event;
29 import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
30 import android.provider.ContactsContract.CommonDataKinds.Im;
31 import android.provider.ContactsContract.CommonDataKinds.Nickname;
32 import android.provider.ContactsContract.CommonDataKinds.Note;
33 import android.provider.ContactsContract.CommonDataKinds.Organization;
34 import android.provider.ContactsContract.CommonDataKinds.Phone;
35 import android.provider.ContactsContract.CommonDataKinds.Photo;
36 import android.provider.ContactsContract.CommonDataKinds.Relation;
37 import android.provider.ContactsContract.CommonDataKinds.SipAddress;
38 import android.provider.ContactsContract.CommonDataKinds.StructuredName;
39 import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
40 import android.provider.ContactsContract.CommonDataKinds.Website;
41 import android.text.TextUtils;
42 import android.util.AttributeSet;
43 import android.util.Log;
44 import android.view.LayoutInflater;
45 import android.view.View;
46 import android.view.ViewGroup;
47 import android.widget.AdapterView;
48 import android.widget.ImageView;
49 import android.widget.LinearLayout;
50 import android.widget.ListPopupWindow;
51 import android.widget.TextView;
52 
53 import com.android.contacts.GeoUtil;
54 import com.android.contacts.R;
55 import com.android.contacts.compat.PhoneNumberUtilsCompat;
56 import com.android.contacts.model.AccountTypeManager;
57 import com.android.contacts.model.RawContactDelta;
58 import com.android.contacts.model.RawContactDeltaList;
59 import com.android.contacts.model.RawContactModifier;
60 import com.android.contacts.model.ValuesDelta;
61 import com.android.contacts.model.account.AccountInfo;
62 import com.android.contacts.model.account.AccountType;
63 import com.android.contacts.model.account.AccountWithDataSet;
64 import com.android.contacts.model.dataitem.CustomDataItem;
65 import com.android.contacts.model.dataitem.DataKind;
66 import com.android.contacts.util.AccountsListAdapter;
67 import com.android.contacts.util.MaterialColorMapUtils;
68 import com.android.contacts.util.UiClosables;
69 
70 import java.io.FileNotFoundException;
71 import java.util.ArrayList;
72 import java.util.Arrays;
73 import java.util.Comparator;
74 import java.util.HashMap;
75 import java.util.List;
76 import java.util.Map;
77 import java.util.Set;
78 import java.util.TreeSet;
79 
80 /**
81  * View to display information from multiple {@link RawContactDelta}s grouped together.
82  */
83 public class RawContactEditorView extends LinearLayout implements View.OnClickListener {
84 
85     static final String TAG = "RawContactEditorView";
86 
87     /**
88      * Callbacks for hosts of {@link RawContactEditorView}s.
89      */
90     public interface Listener {
91 
92         /**
93          * Invoked when the structured name editor field has changed.
94          *
95          * @param rawContactId The raw contact ID from the underlying {@link RawContactDelta}.
96          * @param valuesDelta The values from the underlying {@link RawContactDelta}.
97          */
onNameFieldChanged(long rawContactId, ValuesDelta valuesDelta)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          */
onRebindEditorsForNewContact(RawContactDelta oldState, AccountWithDataSet oldAccount, AccountWithDataSet newAccount)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          */
onBindEditorsFailed()113         public void onBindEditorsFailed();
114 
115         /**
116          * Invoked after editors have been bound for the contact.
117          */
onEditorsBound()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
compare(String mimeType1, String mimeType2)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 
SavedState(Parcelable superState)179         public SavedState(Parcelable superState) {
180             super(superState);
181         }
182 
SavedState(Parcel in)183         private SavedState(Parcel in) {
184             super(in);
185             mIsExpanded = in.readInt() != 0;
186         }
187 
188         @Override
writeToParcel(Parcel out, int flags)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 
RawContactEditorView(Context context)230     public RawContactEditorView(Context context) {
231         super(context);
232     }
233 
RawContactEditorView(Context context, AttributeSet attrs)234     public RawContactEditorView(Context context, AttributeSet attrs) {
235         super(context, attrs);
236     }
237 
238     /**
239      * Sets the receiver for {@link RawContactEditorView} callbacks.
240      */
setListener(Listener listener)241     public void setListener(Listener listener) {
242         mListener = listener;
243     }
244 
245     @Override
onFinishInflate()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
onClick(View view)267     public void onClick(View view) {
268         if (view.getId() == R.id.more_fields) {
269             showAllFields();
270         }
271     }
272 
273     @Override
setEnabled(boolean enabled)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
onSaveInstanceState()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
onRestoreInstanceState(Parcelable state)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      */
setPhotoListener(PhotoEditorView.Listener listener)307     public void setPhotoListener(PhotoEditorView.Listener listener) {
308         mPhotoView.setListener(listener);
309     }
310 
removePhoto()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      */
setFullSizePhoto(Uri photoUri)322     public void setFullSizePhoto(Uri photoUri) {
323         mPhotoView.setFullSizedPhoto(photoUri);
324     }
325 
updatePhoto(Uri photoUri)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 
unsetSuperPrimaryFromAllPhotos()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      */
isWritablePhotoSet()372     public boolean isWritablePhotoSet() {
373         return mPhotoView.isWritablePhotoSet();
374     }
375 
376     /**
377      * Get the raw contact ID for the current photo.
378      */
getPhotoRawContactId()379     public long getPhotoRawContactId() {
380         return mCurrentRawContactDelta == null ? - 1 : mCurrentRawContactDelta.getRawContactId();
381     }
382 
getNameEditorView()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 
getPhoneticEditorView()390     public TextFieldsEditorView getPhoneticEditorView() {
391         final KindSectionView kindSectionView = mKindSectionViewMap
392                 .get(StructuredName.CONTENT_ITEM_TYPE);
393         return kindSectionView == null
394                 ? null : kindSectionView.getPhoneticEditorView();
395     }
396 
getCurrentRawContactDelta()397     public RawContactDelta getCurrentRawContactDelta() {
398         return mCurrentRawContactDelta;
399     }
400 
401     /**
402      * Marks the raw contact photo given as primary for the aggregate contact.
403      */
setPrimaryPhoto()404     public void setPrimaryPhoto() {
405 
406         // Update values delta
407         final ValuesDelta valuesDelta = mCurrentRawContactDelta
408                 .getSuperPrimaryEntry(Photo.CONTENT_ITEM_TYPE);
409         if (valuesDelta == null) {
410             Log.wtf(TAG, "setPrimaryPhoto: had no ValuesDelta for the current RawContactDelta");
411             return;
412         }
413         valuesDelta.setFromTemplate(false);
414         unsetSuperPrimaryFromAllPhotos();
415         valuesDelta.setSuperPrimary(true);
416     }
417 
getAggregationAnchorView()418     public View getAggregationAnchorView() {
419         final StructuredNameEditorView nameEditorView = getNameEditorView();
420         return nameEditorView != null ? nameEditorView.findViewById(R.id.anchor_view) : null;
421     }
422 
setGroupMetaData(Cursor groupMetaData)423     public void setGroupMetaData(Cursor groupMetaData) {
424         final KindSectionView groupKindSectionView =
425                 mKindSectionViewMap.get(GroupMembership.CONTENT_ITEM_TYPE);
426         if (groupKindSectionView == null) {
427             return;
428         }
429         groupKindSectionView.setGroupMetaData(groupMetaData);
430         if (mIsExpanded) {
431             groupKindSectionView.setHideWhenEmpty(false);
432             groupKindSectionView.updateEmptyEditors(/* shouldAnimate =*/ true);
433         }
434     }
435 
setIntentExtras(Bundle extras)436     public void setIntentExtras(Bundle extras) {
437         mIntentExtras = extras;
438     }
439 
setState(RawContactDeltaList rawContactDeltas, MaterialColorMapUtils.MaterialPalette materialPalette, ViewIdGenerator viewIdGenerator, boolean hasNewContact, boolean isUserProfile, AccountWithDataSet primaryAccount, long rawContactIdToDisplayAlone)440     public void setState(RawContactDeltaList rawContactDeltas,
441             MaterialColorMapUtils.MaterialPalette materialPalette, ViewIdGenerator viewIdGenerator,
442             boolean hasNewContact, boolean isUserProfile, AccountWithDataSet primaryAccount,
443             long rawContactIdToDisplayAlone) {
444 
445         mRawContactDeltas = rawContactDeltas;
446         mRawContactIdToDisplayAlone = rawContactIdToDisplayAlone;
447 
448         mKindSectionViewMap.clear();
449         mKindSectionViews.removeAllViews();
450         mMoreFields.setVisibility(View.VISIBLE);
451 
452         mMaterialPalette = materialPalette;
453         mViewIdGenerator = viewIdGenerator;
454 
455         mHasNewContact = hasNewContact;
456         mIsUserProfile = isUserProfile;
457         mPrimaryAccount = primaryAccount;
458         if (mPrimaryAccount == null && mAccounts != null) {
459             mPrimaryAccount = ContactEditorUtils.create(getContext())
460                     .getOnlyOrDefaultAccount(AccountInfo.extractAccounts(mAccounts));
461         }
462         if (Log.isLoggable(TAG, Log.VERBOSE)) {
463             Log.v(TAG, "state: primary " + mPrimaryAccount);
464         }
465 
466         // Parse the given raw contact deltas
467         if (rawContactDeltas == null || rawContactDeltas.isEmpty()) {
468             elog("No raw contact deltas");
469             if (mListener != null) mListener.onBindEditorsFailed();
470             return;
471         }
472         pickRawContactDelta();
473         if (mCurrentRawContactDelta == null) {
474             elog("Couldn't pick a raw contact delta.");
475             if (mListener != null) mListener.onBindEditorsFailed();
476             return;
477         }
478         // Apply any intent extras now that we have selected a raw contact delta.
479         applyIntentExtras();
480         parseRawContactDelta();
481         if (mKindSectionDataMap.isEmpty()) {
482             elog("No kind section data parsed from RawContactDelta(s)");
483             if (mListener != null) mListener.onBindEditorsFailed();
484             return;
485         }
486 
487         final KindSectionData nameSectionData =
488                 mKindSectionDataMap.get(StructuredName.CONTENT_ITEM_TYPE);
489         // Ensure that a structured name and photo exists
490         if (nameSectionData != null) {
491             final RawContactDelta rawContactDelta =
492                     nameSectionData.getRawContactDelta();
493             RawContactModifier.ensureKindExists(
494                     rawContactDelta,
495                     rawContactDelta.getAccountType(mAccountTypeManager),
496                     StructuredName.CONTENT_ITEM_TYPE);
497             RawContactModifier.ensureKindExists(
498                     rawContactDelta,
499                     rawContactDelta.getAccountType(mAccountTypeManager),
500                     Photo.CONTENT_ITEM_TYPE);
501         }
502 
503         // Setup the view
504         addPhotoView();
505         setAccountInfo();
506         if (isReadOnlyRawContact()) {
507             // We're want to display the inputs fields for a single read only raw contact
508             addReadOnlyRawContactEditorViews();
509         } else {
510             setupEditorNormally();
511             // If we're inserting a new contact, request focus to bring up the keyboard for the
512             // name field.
513             if (mHasNewContact) {
514                 final StructuredNameEditorView name = getNameEditorView();
515                 if (name != null) {
516                     name.requestFocusForFirstEditField();
517                 }
518             }
519         }
520         if (mListener != null) mListener.onEditorsBound();
521     }
522 
setAccounts(List<AccountInfo> accounts)523     public void setAccounts(List<AccountInfo> accounts) {
524         mAccounts.clear();
525         mAccounts.addAll(accounts);
526         // Update the account header
527         setAccountInfo();
528     }
529 
setupEditorNormally()530     private void setupEditorNormally() {
531         addKindSectionViews();
532 
533         mMoreFields.setVisibility(hasMoreFields() ? View.VISIBLE : View.GONE);
534 
535         if (mIsExpanded) showAllFields();
536     }
537 
isReadOnlyRawContact()538     private boolean isReadOnlyRawContact() {
539         return !mCurrentRawContactDelta.getAccountType(mAccountTypeManager).areContactsWritable();
540     }
541 
pickRawContactDelta()542     private void pickRawContactDelta() {
543         if (Log.isLoggable(TAG, Log.VERBOSE)) {
544             Log.v(TAG, "parse: " + mRawContactDeltas.size() + " rawContactDelta(s)");
545         }
546         for (int j = 0; j < mRawContactDeltas.size(); j++) {
547             final RawContactDelta rawContactDelta = mRawContactDeltas.get(j);
548             if (Log.isLoggable(TAG, Log.VERBOSE)) {
549                 Log.v(TAG, "parse: " + j + " rawContactDelta" + rawContactDelta);
550             }
551             if (rawContactDelta == null || !rawContactDelta.isVisible()) continue;
552             final AccountType accountType = rawContactDelta.getAccountType(mAccountTypeManager);
553             if (accountType == null) continue;
554 
555             if (mRawContactIdToDisplayAlone > 0) {
556                 // Look for the raw contact if specified.
557                 if (rawContactDelta.getRawContactId().equals(mRawContactIdToDisplayAlone)) {
558                     mCurrentRawContactDelta = rawContactDelta;
559                     return;
560                 }
561             } else if (mPrimaryAccount != null
562                     && mPrimaryAccount.equals(rawContactDelta.getAccountWithDataSet())) {
563                 // Otherwise try to find the one that matches the default.
564                 mCurrentRawContactDelta = rawContactDelta;
565                 return;
566             } else if (accountType.areContactsWritable()){
567                 // TODO: Find better raw contact delta
568                 // Just select an arbitrary writable contact.
569                 mCurrentRawContactDelta = rawContactDelta;
570             }
571         }
572 
573     }
574 
applyIntentExtras()575     private void applyIntentExtras() {
576         if (mIntentExtras == null || mIntentExtras.size() == 0) {
577             return;
578         }
579         final AccountTypeManager accountTypes = AccountTypeManager.getInstance(getContext());
580         final AccountType type = mCurrentRawContactDelta.getAccountType(accountTypes);
581 
582         RawContactModifier.parseExtras(getContext(), type, mCurrentRawContactDelta, mIntentExtras);
583         mIntentExtras = null;
584     }
585 
parseRawContactDelta()586     private void parseRawContactDelta() {
587         mKindSectionDataMap.clear();
588         mSortedMimetypes.clear();
589 
590         final AccountType accountType = mCurrentRawContactDelta.getAccountType(mAccountTypeManager);
591         final List<DataKind> dataKinds = accountType.getSortedDataKinds();
592         final int dataKindSize = dataKinds == null ? 0 : dataKinds.size();
593         if (Log.isLoggable(TAG, Log.VERBOSE)) {
594             Log.v(TAG, "parse: " + dataKindSize + " dataKinds(s)");
595         }
596 
597         for (int i = 0; i < dataKindSize; i++) {
598             final DataKind dataKind = dataKinds.get(i);
599             // Skip null and un-editable fields.
600             if (dataKind == null || !dataKind.editable) {
601                 if (Log.isLoggable(TAG, Log.VERBOSE)) {
602                     Log.v(TAG, "parse: " + i +
603                             (dataKind == null ? " dropped null data kind"
604                                     : " dropped uneditable mimetype: " + dataKind.mimeType));
605                 }
606                 continue;
607             }
608             final String mimeType = dataKind.mimeType;
609 
610             // Skip psuedo mime types
611             if (DataKind.PSEUDO_MIME_TYPE_NAME.equals(mimeType) ||
612                     DataKind.PSEUDO_MIME_TYPE_PHONETIC_NAME.equals(mimeType)) {
613                 if (Log.isLoggable(TAG, Log.VERBOSE)) {
614                     Log.v(TAG, "parse: " + i + " " + dataKind.mimeType + " dropped pseudo type");
615                 }
616                 continue;
617             }
618 
619             // Skip custom fields
620             // TODO: Handle them when we implement editing custom fields.
621             if (CustomDataItem.MIMETYPE_CUSTOM_FIELD.equals(mimeType)) {
622                 if (Log.isLoggable(TAG, Log.VERBOSE)) {
623                     Log.v(TAG, "parse: " + i + " " + dataKind.mimeType + " dropped custom field");
624                 }
625                 continue;
626             }
627 
628             final KindSectionData kindSectionData =
629                     new KindSectionData(accountType, dataKind, mCurrentRawContactDelta);
630             mKindSectionDataMap.put(mimeType, kindSectionData);
631             mSortedMimetypes.add(mimeType);
632 
633             if (Log.isLoggable(TAG, Log.VERBOSE)) {
634                 Log.v(TAG, "parse: " + i + " " + dataKind.mimeType + " " +
635                         kindSectionData.getValuesDeltas().size() + " value(s) " +
636                         kindSectionData.getNonEmptyValuesDeltas().size() + " non-empty value(s) " +
637                         kindSectionData.getVisibleValuesDeltas().size() +
638                         " visible value(s)");
639             }
640         }
641     }
642 
addReadOnlyRawContactEditorViews()643     private void addReadOnlyRawContactEditorViews() {
644         mKindSectionViews.removeAllViews();
645         final AccountTypeManager accountTypes = AccountTypeManager.getInstance(
646                 getContext());
647         final AccountType type = mCurrentRawContactDelta.getAccountType(accountTypes);
648 
649         // Bail if invalid state or source
650         if (type == null) return;
651 
652         // Make sure we have StructuredName
653         RawContactModifier.ensureKindExists(
654                 mCurrentRawContactDelta, type, StructuredName.CONTENT_ITEM_TYPE);
655 
656         ValuesDelta primary;
657 
658         // Name
659         final Context context = getContext();
660         final Resources res = context.getResources();
661         primary = mCurrentRawContactDelta.getPrimaryEntry(StructuredName.CONTENT_ITEM_TYPE);
662         final String name = primary != null ? primary.getAsString(StructuredName.DISPLAY_NAME) :
663             getContext().getString(R.string.missing_name);
664         final Drawable nameDrawable = context.getDrawable(R.drawable.quantum_ic_person_vd_theme_24);
665         final String nameContentDescription = res.getString(R.string.header_name_entry);
666         bindData(nameDrawable, nameContentDescription, name, /* type */ null,
667                 /* isFirstEntry */ true);
668 
669         // Phones
670         final ArrayList<ValuesDelta> phones = mCurrentRawContactDelta
671                 .getMimeEntries(Phone.CONTENT_ITEM_TYPE);
672         final Drawable phoneDrawable = context.getDrawable(R.drawable.quantum_ic_phone_vd_theme_24);
673         final String phoneContentDescription = res.getString(R.string.header_phone_entry);
674         if (phones != null) {
675             boolean isFirstPhoneBound = true;
676             for (ValuesDelta phone : phones) {
677                 final String phoneNumber = phone.getPhoneNumber();
678                 if (TextUtils.isEmpty(phoneNumber)) {
679                     continue;
680                 }
681                 final String formattedNumber = PhoneNumberUtilsCompat.formatNumber(
682                         phoneNumber, phone.getPhoneNormalizedNumber(),
683                         GeoUtil.getCurrentCountryIso(getContext()));
684                 CharSequence phoneType = null;
685                 if (phone.hasPhoneType()) {
686                     phoneType = Phone.getTypeLabel(
687                             res, phone.getPhoneType(), phone.getPhoneLabel());
688                 }
689                 bindData(phoneDrawable, phoneContentDescription, formattedNumber, phoneType,
690                         isFirstPhoneBound, true);
691                 isFirstPhoneBound = false;
692             }
693         }
694 
695         // Emails
696         final ArrayList<ValuesDelta> emails = mCurrentRawContactDelta
697                 .getMimeEntries(Email.CONTENT_ITEM_TYPE);
698         final Drawable emailDrawable = context.getDrawable(R.drawable.quantum_ic_email_vd_theme_24);
699         final String emailContentDescription = res.getString(R.string.header_email_entry);
700         if (emails != null) {
701             boolean isFirstEmailBound = true;
702             for (ValuesDelta email : emails) {
703                 final String emailAddress = email.getEmailData();
704                 if (TextUtils.isEmpty(emailAddress)) {
705                     continue;
706                 }
707                 CharSequence emailType = null;
708                 if (email.hasEmailType()) {
709                     emailType = Email.getTypeLabel(
710                             res, email.getEmailType(), email.getEmailLabel());
711                 }
712                 bindData(emailDrawable, emailContentDescription, emailAddress, emailType,
713                         isFirstEmailBound);
714                 isFirstEmailBound = false;
715             }
716         }
717 
718         mKindSectionViews.setVisibility(mKindSectionViews.getChildCount() > 0 ? VISIBLE : GONE);
719         // Hide the "More fields" link
720         mMoreFields.setVisibility(GONE);
721     }
722 
bindData(Drawable icon, String iconContentDescription, CharSequence data, CharSequence type, boolean isFirstEntry)723     private void bindData(Drawable icon, String iconContentDescription, CharSequence data,
724             CharSequence type, boolean isFirstEntry) {
725         bindData(icon, iconContentDescription, data, type, isFirstEntry, false);
726     }
727 
bindData(Drawable icon, String iconContentDescription, CharSequence data, CharSequence type, boolean isFirstEntry, boolean forceLTR)728     private void bindData(Drawable icon, String iconContentDescription, CharSequence data,
729             CharSequence type, boolean isFirstEntry, boolean forceLTR) {
730         final View field = mLayoutInflater.inflate(R.layout.item_read_only_field, mKindSectionViews,
731                 /* attachToRoot */ false);
732         if (isFirstEntry) {
733             final ImageView imageView = (ImageView) field.findViewById(R.id.kind_icon);
734             imageView.setImageDrawable(icon);
735             imageView.setContentDescription(iconContentDescription);
736         } else {
737             final ImageView imageView = (ImageView) field.findViewById(R.id.kind_icon);
738             imageView.setVisibility(View.INVISIBLE);
739             imageView.setContentDescription(null);
740         }
741         final TextView dataView = (TextView) field.findViewById(R.id.data);
742         dataView.setText(data);
743         if (forceLTR) {
744             dataView.setTextDirection(View.TEXT_DIRECTION_LTR);
745         }
746         final TextView typeView = (TextView) field.findViewById(R.id.type);
747         if (!TextUtils.isEmpty(type)) {
748             typeView.setText(type);
749         } else {
750             typeView.setVisibility(View.GONE);
751         }
752         mKindSectionViews.addView(field);
753     }
754 
setAccountInfo()755     private void setAccountInfo() {
756         if (mCurrentRawContactDelta == null && mPrimaryAccount == null) {
757             return;
758         }
759         final AccountTypeManager accountTypeManager = AccountTypeManager.getInstance(getContext());
760         final AccountInfo account = mCurrentRawContactDelta != null
761                 ? accountTypeManager.getAccountInfoForAccount(
762                 mCurrentRawContactDelta.getAccountWithDataSet())
763                 : accountTypeManager.getAccountInfoForAccount(mPrimaryAccount);
764 
765         // Accounts haven't loaded yet or we are editing.
766         if (mAccounts.isEmpty()) {
767             mAccounts.add(account);
768         }
769 
770         // Get the account information for the primary raw contact delta
771         if (isReadOnlyRawContact()) {
772             final String accountType = account.getTypeLabel().toString();
773             setAccountHeader(accountType,
774                     getResources().getString(
775                             R.string.editor_account_selector_read_only_title, accountType));
776         } else {
777             final String accountLabel = mIsUserProfile
778                     ? EditorUiUtils.getAccountHeaderLabelForMyProfile(getContext(), account)
779                     : account.getNameLabel().toString();
780             setAccountHeader(getResources().getString(R.string.editor_account_selector_title),
781                     accountLabel);
782         }
783 
784         // If we're saving a new contact and there are multiple accounts, add the account selector.
785         if (mHasNewContact && !mIsUserProfile && mAccounts.size() > 1) {
786             addAccountSelector(mCurrentRawContactDelta);
787         }
788     }
789 
setAccountHeader(String primaryText, String secondaryText)790     private void setAccountHeader(String primaryText, String secondaryText) {
791         mAccountHeaderPrimaryText.setText(primaryText);
792         mAccountHeaderSecondaryText.setText(secondaryText);
793 
794         // Set the icon
795         final AccountType accountType =
796                 mCurrentRawContactDelta.getRawContactAccountType(getContext());
797         mAccountHeaderIcon.setImageDrawable(accountType.getDisplayIcon(getContext()));
798 
799         // Set the content description
800         mAccountHeaderContainer.setContentDescription(
801                 EditorUiUtils.getAccountInfoContentDescription(secondaryText, primaryText));
802     }
803 
addAccountSelector(final RawContactDelta rawContactDelta)804     private void addAccountSelector(final RawContactDelta rawContactDelta) {
805         // Add handlers for choosing another account to save to.
806         mAccountHeaderExpanderIcon.setVisibility(View.VISIBLE);
807         final OnClickListener clickListener = new OnClickListener() {
808             @Override
809             public void onClick(View v) {
810                 final AccountWithDataSet current = rawContactDelta.getAccountWithDataSet();
811                 AccountInfo.sortAccounts(current, mAccounts);
812                 final ListPopupWindow popup = new ListPopupWindow(getContext(), null);
813                 final AccountsListAdapter adapter =
814                         new AccountsListAdapter(getContext(), mAccounts, current);
815                 popup.setWidth(mAccountHeaderContainer.getWidth());
816                 popup.setAnchorView(mAccountHeaderContainer);
817                 popup.setAdapter(adapter);
818                 popup.setModal(true);
819                 popup.setInputMethodMode(ListPopupWindow.INPUT_METHOD_NOT_NEEDED);
820                 popup.setOnItemClickListener(new AdapterView.OnItemClickListener() {
821                     @Override
822                     public void onItemClick(AdapterView<?> parent, View view, int position,
823                             long id) {
824                         UiClosables.closeQuietly(popup);
825                         final AccountWithDataSet newAccount = adapter.getItem(position);
826                         if (mListener != null && !mPrimaryAccount.equals(newAccount)) {
827                             mIsExpanded = false;
828                             mListener.onRebindEditorsForNewContact(
829                                     rawContactDelta,
830                                     mPrimaryAccount,
831                                     newAccount);
832                         }
833                     }
834                 });
835                 popup.show();
836             }
837         };
838         mAccountHeaderContainer.setOnClickListener(clickListener);
839         // Make the expander icon clickable so that it will be announced as a button by
840         // talkback
841         mAccountHeaderExpanderIcon.setOnClickListener(clickListener);
842     }
843 
addPhotoView()844     private void addPhotoView() {
845         if (!mCurrentRawContactDelta.hasMimeEntries(Photo.CONTENT_ITEM_TYPE)) {
846             wlog("No photo mimetype for this raw contact.");
847             mPhotoView.setVisibility(GONE);
848             return;
849         } else {
850             mPhotoView.setVisibility(VISIBLE);
851         }
852 
853         final ValuesDelta superPrimaryDelta = mCurrentRawContactDelta
854                 .getSuperPrimaryEntry(Photo.CONTENT_ITEM_TYPE);
855         if (superPrimaryDelta == null) {
856             Log.wtf(TAG, "addPhotoView: no ValueDelta found for current RawContactDelta"
857                     + "that supports a photo.");
858             mPhotoView.setVisibility(GONE);
859             return;
860         }
861         // Set the photo view
862         mPhotoView.setPalette(mMaterialPalette);
863         mPhotoView.setPhoto(superPrimaryDelta);
864 
865         if (isReadOnlyRawContact()) {
866             mPhotoView.setReadOnly(true);
867             return;
868         }
869         mPhotoView.setReadOnly(false);
870         mPhotoValuesDelta = superPrimaryDelta;
871     }
872 
addKindSectionViews()873     private void addKindSectionViews() {
874         int i = -1;
875 
876         for (String mimeType : mSortedMimetypes) {
877             i++;
878             // Ignore mime types that we've already handled
879             if (Photo.CONTENT_ITEM_TYPE.equals(mimeType)) {
880                 if (Log.isLoggable(TAG, Log.VERBOSE)) {
881                     Log.v(TAG, "kind: " + i + " " + mimeType + " dropped");
882                 }
883                 continue;
884             }
885             final KindSectionView kindSectionView;
886             final KindSectionData kindSectionData = mKindSectionDataMap.get(mimeType);
887             kindSectionView = inflateKindSectionView(mKindSectionViews, kindSectionData, mimeType);
888             mKindSectionViews.addView(kindSectionView);
889 
890             // Keep a pointer to the KindSectionView for each mimeType
891             mKindSectionViewMap.put(mimeType, kindSectionView);
892         }
893     }
894 
inflateKindSectionView(ViewGroup viewGroup, KindSectionData kindSectionData, String mimeType)895     private KindSectionView inflateKindSectionView(ViewGroup viewGroup,
896             KindSectionData kindSectionData, String mimeType) {
897         final KindSectionView kindSectionView = (KindSectionView)
898                 mLayoutInflater.inflate(R.layout.item_kind_section, viewGroup,
899                         /* attachToRoot =*/ false);
900         kindSectionView.setIsUserProfile(mIsUserProfile);
901 
902         if (Phone.CONTENT_ITEM_TYPE.equals(mimeType)
903                 || Email.CONTENT_ITEM_TYPE.equals(mimeType)) {
904             // Phone numbers and email addresses are always displayed,
905             // even if they are empty
906             kindSectionView.setHideWhenEmpty(false);
907         }
908 
909         // Since phone numbers and email addresses displayed even if they are empty,
910         // they will be the only types you add new values to initially for new contacts
911         kindSectionView.setShowOneEmptyEditor(true);
912 
913         kindSectionView.setState(kindSectionData, mViewIdGenerator, mListener);
914 
915         return kindSectionView;
916     }
917 
showAllFields()918     private void showAllFields() {
919         // Stop hiding empty editors and allow the user to enter values for all kinds now
920         for (int i = 0; i < mKindSectionViews.getChildCount(); i++) {
921             final KindSectionView kindSectionView =
922                     (KindSectionView) mKindSectionViews.getChildAt(i);
923             kindSectionView.setHideWhenEmpty(false);
924             kindSectionView.updateEmptyEditors(/* shouldAnimate =*/ true);
925         }
926         mIsExpanded = true;
927 
928         // Hide the more fields button
929         mMoreFields.setVisibility(View.GONE);
930     }
931 
hasMoreFields()932     private boolean hasMoreFields() {
933         for (KindSectionView section : mKindSectionViewMap.values()) {
934             if (section.getVisibility() != View.VISIBLE) {
935                 return true;
936             }
937         }
938         return false;
939     }
940 
wlog(String message)941     private static void wlog(String message) {
942         if (Log.isLoggable(TAG, Log.WARN)) {
943             Log.w(TAG, message);
944         }
945     }
946 
elog(String message)947     private static void elog(String message) {
948         Log.e(TAG, message);
949     }
950 }
951