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