• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
getCurrentRawContactDelta()390     public RawContactDelta getCurrentRawContactDelta() {
391         return mCurrentRawContactDelta;
392     }
393 
394     /**
395      * Marks the raw contact photo given as primary for the aggregate contact.
396      */
setPrimaryPhoto()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 
getAggregationAnchorView()411     public View getAggregationAnchorView() {
412         final StructuredNameEditorView nameEditorView = getNameEditorView();
413         return nameEditorView != null ? nameEditorView.findViewById(R.id.anchor_view) : null;
414     }
415 
setGroupMetaData(Cursor groupMetaData)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 
setIntentExtras(Bundle extras)429     public void setIntentExtras(Bundle extras) {
430         mIntentExtras = extras;
431     }
432 
setState(RawContactDeltaList rawContactDeltas, MaterialColorMapUtils.MaterialPalette materialPalette, ViewIdGenerator viewIdGenerator, boolean hasNewContact, boolean isUserProfile, AccountWithDataSet primaryAccount, long rawContactIdToDisplayAlone)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 
setAccounts(List<AccountInfo> accounts)516     public void setAccounts(List<AccountInfo> accounts) {
517         mAccounts.clear();
518         mAccounts.addAll(accounts);
519         // Update the account header
520         setAccountInfo();
521     }
522 
setupEditorNormally()523     private void setupEditorNormally() {
524         addKindSectionViews();
525 
526         mMoreFields.setVisibility(hasMoreFields() ? View.VISIBLE : View.GONE);
527 
528         if (mIsExpanded) showAllFields();
529     }
530 
isReadOnlyRawContact()531     private boolean isReadOnlyRawContact() {
532         return !mCurrentRawContactDelta.getAccountType(mAccountTypeManager).areContactsWritable();
533     }
534 
pickRawContactDelta()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 
applyIntentExtras()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 
parseRawContactDelta()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 
addReadOnlyRawContactEditorViews()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 
bindData(Drawable icon, String iconContentDescription, CharSequence data, CharSequence type, boolean isFirstEntry)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 
bindData(Drawable icon, String iconContentDescription, CharSequence data, CharSequence type, boolean isFirstEntry, boolean forceLTR)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 
setAccountInfo()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 
setAccountHeader(String primaryText, String secondaryText)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 
addAccountSelector(final RawContactDelta rawContactDelta)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 
addPhotoView()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 
addKindSectionViews()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 
inflateKindSectionView(ViewGroup viewGroup, KindSectionData kindSectionData, String mimeType)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 
showAllFields()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 
hasMoreFields()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 
wlog(String message)934     private static void wlog(String message) {
935         if (Log.isLoggable(TAG, Log.WARN)) {
936             Log.w(TAG, message);
937         }
938     }
939 
elog(String message)940     private static void elog(String message) {
941         Log.e(TAG, message);
942     }
943 }
944