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 com.android.contacts.R;
20 import com.android.contacts.common.model.AccountTypeManager;
21 import com.android.contacts.common.model.RawContactDelta;
22 import com.android.contacts.common.model.RawContactDeltaList;
23 import com.android.contacts.common.model.RawContactModifier;
24 import com.android.contacts.common.model.ValuesDelta;
25 import com.android.contacts.common.model.account.AccountType;
26 import com.android.contacts.common.model.account.AccountWithDataSet;
27 import com.android.contacts.common.model.dataitem.DataKind;
28 import com.android.contacts.common.util.AccountsListAdapter;
29 import com.android.contacts.common.util.MaterialColorMapUtils;
30 import com.android.contacts.util.UiClosables;
31 
32 import android.content.ContentUris;
33 import android.content.Context;
34 import android.database.Cursor;
35 import android.graphics.Bitmap;
36 import android.net.Uri;
37 import android.os.Bundle;
38 import android.os.Parcel;
39 import android.os.Parcelable;
40 import android.provider.ContactsContract;
41 import android.provider.ContactsContract.CommonDataKinds.Email;
42 import android.provider.ContactsContract.CommonDataKinds.Event;
43 import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
44 import android.provider.ContactsContract.CommonDataKinds.Im;
45 import android.provider.ContactsContract.CommonDataKinds.Nickname;
46 import android.provider.ContactsContract.CommonDataKinds.Note;
47 import android.provider.ContactsContract.CommonDataKinds.Organization;
48 import android.provider.ContactsContract.CommonDataKinds.Phone;
49 import android.provider.ContactsContract.CommonDataKinds.Photo;
50 import android.provider.ContactsContract.CommonDataKinds.Relation;
51 import android.provider.ContactsContract.CommonDataKinds.SipAddress;
52 import android.provider.ContactsContract.CommonDataKinds.StructuredName;
53 import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
54 import android.provider.ContactsContract.CommonDataKinds.Website;
55 import android.text.TextUtils;
56 import android.util.AttributeSet;
57 import android.util.Log;
58 import android.util.Pair;
59 import android.view.LayoutInflater;
60 import android.view.View;
61 import android.view.ViewGroup;
62 import android.widget.AdapterView;
63 import android.widget.BaseAdapter;
64 import android.widget.ImageView;
65 import android.widget.LinearLayout;
66 import android.widget.ListPopupWindow;
67 import android.widget.TextView;
68 
69 import java.io.FileNotFoundException;
70 import java.util.ArrayList;
71 import java.util.Arrays;
72 import java.util.Collections;
73 import java.util.Comparator;
74 import java.util.HashMap;
75 import java.util.List;
76 import java.util.Map;
77 import java.util.TreeSet;
78 
79 /**
80  * View to display information from multiple {@link RawContactDelta}s grouped together.
81  */
82 public class CompactRawContactsEditorView extends LinearLayout implements View.OnClickListener {
83 
84     static final String TAG = "CompactEditorView";
85 
86     private static final KindSectionDataMapEntryComparator
87             KIND_SECTION_DATA_MAP_ENTRY_COMPARATOR = new KindSectionDataMapEntryComparator();
88 
89     /**
90      * Callbacks for hosts of {@link CompactRawContactsEditorView}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 compact 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          * Invoked when a rawcontact from linked contacts is selected in editor.
124          */
onRawContactSelected(Uri uri, long rawContactId, boolean isReadOnly)125         public void onRawContactSelected(Uri uri, long rawContactId, boolean isReadOnly);
126 
127         /**
128          * Returns the map of raw contact IDs to newly taken or selected photos that have not
129          * yet been saved to CP2.
130          */
getUpdatedPhotos()131         public Bundle getUpdatedPhotos();
132     }
133 
134     /**
135      * Used to list the account info for the given raw contacts list.
136      */
137     private static final class RawContactAccountListAdapter extends BaseAdapter {
138         private final LayoutInflater mInflater;
139         private final Context mContext;
140         private final RawContactDeltaList mRawContactDeltas;
141 
RawContactAccountListAdapter(Context context, RawContactDeltaList rawContactDeltas)142         public RawContactAccountListAdapter(Context context, RawContactDeltaList rawContactDeltas) {
143             mContext = context;
144             mRawContactDeltas = new RawContactDeltaList();
145             for (RawContactDelta rawContactDelta : rawContactDeltas) {
146                 if (rawContactDelta.isVisible() && rawContactDelta.getRawContactId() > 0) {
147                     mRawContactDeltas.add(rawContactDelta);
148                 }
149             }
150             mInflater = LayoutInflater.from(context);
151         }
152 
153         @Override
getView(int position, View convertView, ViewGroup parent)154         public View getView(int position, View convertView, ViewGroup parent) {
155             final View resultView = convertView != null ? convertView
156                     : mInflater.inflate(R.layout.account_selector_list_item, parent, false);
157 
158             final RawContactDelta rawContactDelta = mRawContactDeltas.get(position);
159 
160             final TextView text1 = (TextView) resultView.findViewById(android.R.id.text1);
161             final AccountType accountType = rawContactDelta.getRawContactAccountType(mContext);
162             text1.setText(accountType.getDisplayLabel(mContext));
163 
164             final TextView text2 = (TextView) resultView.findViewById(android.R.id.text2);
165             final String accountName = rawContactDelta.getAccountName();
166             if (TextUtils.isEmpty(accountName)) {
167                 text2.setVisibility(View.GONE);
168             } else {
169                 // Truncate email addresses in the middle so we don't lose the domain
170                 text2.setText(accountName);
171                 text2.setEllipsize(TextUtils.TruncateAt.MIDDLE);
172             }
173 
174             final ImageView icon = (ImageView) resultView.findViewById(android.R.id.icon);
175             icon.setImageDrawable(accountType.getDisplayIcon(mContext));
176 
177             return resultView;
178         }
179 
180         @Override
getCount()181         public int getCount() {
182             return mRawContactDeltas.size();
183         }
184 
185         @Override
getItem(int position)186         public RawContactDelta getItem(int position) {
187             return mRawContactDeltas.get(position);
188         }
189 
190         @Override
getItemId(int position)191         public long getItemId(int position) {
192             return getItem(position).getRawContactId();
193         }
194     }
195 
196     /** Used to sort entire kind sections. */
197     private static final class KindSectionDataMapEntryComparator implements
198             Comparator<Map.Entry<String,KindSectionDataList>> {
199 
200         final MimeTypeComparator mMimeTypeComparator = new MimeTypeComparator();
201 
202         @Override
compare(Map.Entry<String, KindSectionDataList> entry1, Map.Entry<String, KindSectionDataList> entry2)203         public int compare(Map.Entry<String, KindSectionDataList> entry1,
204                 Map.Entry<String, KindSectionDataList> entry2) {
205             if (entry1 == entry2) return 0;
206             if (entry1 == null) return -1;
207             if (entry2 == null) return 1;
208 
209             final String mimeType1 = entry1.getKey();
210             final String mimeType2 = entry2.getKey();
211 
212             return mMimeTypeComparator.compare(mimeType1, mimeType2);
213         }
214     }
215 
216     /**
217      * Sorts kinds roughly the same as quick contacts; we diverge in the following ways:
218      * <ol>
219      *     <li>All names are together at the top.</li>
220      *     <li>IM is moved up after addresses</li>
221      *     <li>SIP addresses are moved to below phone numbers</li>
222      *     <li>Group membership is placed at the end</li>
223      * </ol>
224      */
225     private static final class MimeTypeComparator implements Comparator<String> {
226 
227         private static final List<String> MIME_TYPE_ORDER = Arrays.asList(new String[] {
228                 StructuredName.CONTENT_ITEM_TYPE,
229                 Nickname.CONTENT_ITEM_TYPE,
230                 Organization.CONTENT_ITEM_TYPE,
231                 Phone.CONTENT_ITEM_TYPE,
232                 SipAddress.CONTENT_ITEM_TYPE,
233                 Email.CONTENT_ITEM_TYPE,
234                 StructuredPostal.CONTENT_ITEM_TYPE,
235                 Im.CONTENT_ITEM_TYPE,
236                 Website.CONTENT_ITEM_TYPE,
237                 Event.CONTENT_ITEM_TYPE,
238                 Relation.CONTENT_ITEM_TYPE,
239                 Note.CONTENT_ITEM_TYPE,
240                 GroupMembership.CONTENT_ITEM_TYPE
241         });
242 
243         @Override
compare(String mimeType1, String mimeType2)244         public int compare(String mimeType1, String mimeType2) {
245             if (mimeType1 == mimeType2) return 0;
246             if (mimeType1 == null) return -1;
247             if (mimeType2 == null) return 1;
248 
249             int index1 = MIME_TYPE_ORDER.indexOf(mimeType1);
250             int index2 = MIME_TYPE_ORDER.indexOf(mimeType2);
251 
252             // Fallback to alphabetical ordering of the mime type if both are not found
253             if (index1 < 0 && index2 < 0) return mimeType1.compareTo(mimeType2);
254             if (index1 < 0) return 1;
255             if (index2 < 0) return -1;
256 
257             return index1 < index2 ? -1 : 1;
258         }
259     }
260 
261     /**
262      * Sorts primary accounts and google account types before others.
263      */
264     private static final class EditorComparator implements Comparator<KindSectionData> {
265 
266         private RawContactDeltaComparator mRawContactDeltaComparator;
267 
EditorComparator(Context context)268         private EditorComparator(Context context) {
269             mRawContactDeltaComparator = new RawContactDeltaComparator(context);
270         }
271 
272         @Override
compare(KindSectionData kindSectionData1, KindSectionData kindSectionData2)273         public int compare(KindSectionData kindSectionData1, KindSectionData kindSectionData2) {
274             if (kindSectionData1 == kindSectionData2) return 0;
275             if (kindSectionData1 == null) return -1;
276             if (kindSectionData2 == null) return 1;
277 
278             final RawContactDelta rawContactDelta1 = kindSectionData1.getRawContactDelta();
279             final RawContactDelta rawContactDelta2 = kindSectionData2.getRawContactDelta();
280 
281             if (rawContactDelta1 == rawContactDelta2) return 0;
282             if (rawContactDelta1 == null) return -1;
283             if (rawContactDelta2 == null) return 1;
284 
285             return mRawContactDeltaComparator.compare(rawContactDelta1, rawContactDelta2);
286         }
287     }
288 
289     public static class SavedState extends BaseSavedState {
290 
291         public static final Parcelable.Creator<SavedState> CREATOR =
292                 new Parcelable.Creator<SavedState>() {
293                     public SavedState createFromParcel(Parcel in) {
294                         return new SavedState(in);
295                     }
296                     public SavedState[] newArray(int size) {
297                         return new SavedState[size];
298                     }
299                 };
300 
301         private boolean mIsExpanded;
302 
SavedState(Parcelable superState)303         public SavedState(Parcelable superState) {
304             super(superState);
305         }
306 
SavedState(Parcel in)307         private SavedState(Parcel in) {
308             super(in);
309             mIsExpanded = in.readInt() != 0;
310         }
311 
312         @Override
writeToParcel(Parcel out, int flags)313         public void writeToParcel(Parcel out, int flags) {
314             super.writeToParcel(out, flags);
315             out.writeInt(mIsExpanded ? 1 : 0);
316         }
317     }
318 
319     private CompactRawContactsEditorView.Listener mListener;
320 
321     private AccountTypeManager mAccountTypeManager;
322     private LayoutInflater mLayoutInflater;
323 
324     private ViewIdGenerator mViewIdGenerator;
325     private MaterialColorMapUtils.MaterialPalette mMaterialPalette;
326     private long mPhotoId = -1;
327     private boolean mHasNewContact;
328     private boolean mIsUserProfile;
329     private AccountWithDataSet mPrimaryAccount;
330     private Map<String,KindSectionDataList> mKindSectionDataMap = new HashMap<>();
331 
332     // Account header
333     private View mAccountHeaderContainer;
334     private TextView mAccountHeaderType;
335     private TextView mAccountHeaderName;
336     private ImageView mAccountHeaderIcon;
337 
338     // Account selector
339     private View mAccountSelectorContainer;
340     private View mAccountSelector;
341     private TextView mAccountSelectorType;
342     private TextView mAccountSelectorName;
343 
344     // Raw contacts selector
345     private View mRawContactContainer;
346     private TextView mRawContactSummary;
347 
348     private CompactPhotoEditorView mPhotoView;
349     private ViewGroup mKindSectionViews;
350     private Map<String,List<CompactKindSectionView>> mKindSectionViewsMap = new HashMap<>();
351     private View mMoreFields;
352 
353     private boolean mIsExpanded;
354 
355     private long mPhotoRawContactId;
356     private ValuesDelta mPhotoValuesDelta;
357 
358     private Pair<KindSectionData, ValuesDelta> mPrimaryNameKindSectionData;
359 
CompactRawContactsEditorView(Context context)360     public CompactRawContactsEditorView(Context context) {
361         super(context);
362     }
363 
CompactRawContactsEditorView(Context context, AttributeSet attrs)364     public CompactRawContactsEditorView(Context context, AttributeSet attrs) {
365         super(context, attrs);
366     }
367 
368     /**
369      * Sets the receiver for {@link CompactRawContactsEditorView} callbacks.
370      */
setListener(Listener listener)371     public void setListener(Listener listener) {
372         mListener = listener;
373     }
374 
375     @Override
onFinishInflate()376     protected void onFinishInflate() {
377         super.onFinishInflate();
378 
379         mAccountTypeManager = AccountTypeManager.getInstance(getContext());
380         mLayoutInflater = (LayoutInflater)
381                 getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
382 
383         // Account header
384         mAccountHeaderContainer = findViewById(R.id.account_container);
385         mAccountHeaderType = (TextView) findViewById(R.id.account_type);
386         mAccountHeaderName = (TextView) findViewById(R.id.account_name);
387         mAccountHeaderIcon = (ImageView) findViewById(R.id.account_type_icon);
388 
389         // Account selector
390         mAccountSelectorContainer = findViewById(R.id.account_selector_container);
391         mAccountSelector = findViewById(R.id.account);
392         mAccountSelectorType = (TextView) findViewById(R.id.account_type_selector);
393         mAccountSelectorName = (TextView) findViewById(R.id.account_name_selector);
394 
395         // Raw contacts selector
396         mRawContactContainer = findViewById(R.id.all_rawcontacts_accounts_container);
397         mRawContactSummary = (TextView) findViewById(R.id.rawcontacts_accounts_summary);
398 
399         mPhotoView = (CompactPhotoEditorView) findViewById(R.id.photo_editor);
400         mKindSectionViews = (LinearLayout) findViewById(R.id.kind_section_views);
401         mMoreFields = findViewById(R.id.more_fields);
402         mMoreFields.setOnClickListener(this);
403     }
404 
405     @Override
onClick(View view)406     public void onClick(View view) {
407         if (view.getId() == R.id.more_fields) {
408             showAllFields();
409         }
410     }
411 
412     @Override
setEnabled(boolean enabled)413     public void setEnabled(boolean enabled) {
414         super.setEnabled(enabled);
415         final int childCount = mKindSectionViews.getChildCount();
416         for (int i = 0; i < childCount; i++) {
417             mKindSectionViews.getChildAt(i).setEnabled(enabled);
418         }
419     }
420 
421     @Override
onSaveInstanceState()422     public Parcelable onSaveInstanceState() {
423         final Parcelable superState = super.onSaveInstanceState();
424         final SavedState savedState = new SavedState(superState);
425         savedState.mIsExpanded = mIsExpanded;
426         return savedState;
427     }
428 
429     @Override
onRestoreInstanceState(Parcelable state)430     public void onRestoreInstanceState(Parcelable state) {
431         if(!(state instanceof SavedState)) {
432             super.onRestoreInstanceState(state);
433             return;
434         }
435         final SavedState savedState = (SavedState) state;
436         super.onRestoreInstanceState(savedState.getSuperState());
437         mIsExpanded = savedState.mIsExpanded;
438         if (mIsExpanded) {
439             showAllFields();
440         }
441     }
442 
443     /**
444      * Pass through to {@link CompactPhotoEditorView#setListener}.
445      */
setPhotoListener(CompactPhotoEditorView.Listener listener)446     public void setPhotoListener(CompactPhotoEditorView.Listener listener) {
447         mPhotoView.setListener(listener);
448     }
449 
removePhoto()450     public void removePhoto() {
451         mPhotoValuesDelta.setFromTemplate(true);
452         mPhotoValuesDelta.put(Photo.PHOTO, (byte[]) null);
453 
454         mPhotoView.removePhoto();
455     }
456 
457     /**
458      * Pass through to {@link CompactPhotoEditorView#setFullSizedPhoto(Uri)}.
459      */
setFullSizePhoto(Uri photoUri)460     public void setFullSizePhoto(Uri photoUri) {
461         mPhotoView.setFullSizedPhoto(photoUri);
462     }
463 
updatePhoto(Uri photoUri)464     public void updatePhoto(Uri photoUri) {
465         mPhotoValuesDelta.setFromTemplate(false);
466         // Unset primary for all photos
467         unsetSuperPrimaryFromAllPhotos();
468         // Mark the currently displayed photo as primary
469         mPhotoValuesDelta.setSuperPrimary(true);
470 
471         // Even though high-res photos cannot be saved by passing them via
472         // an EntityDeltaList (since they cause the Bundle size limit to be
473         // exceeded), we still pass a low-res thumbnail. This simplifies
474         // code all over the place, because we don't have to test whether
475         // there is a change in EITHER the delta-list OR a changed photo...
476         // this way, there is always a change in the delta-list.
477         try {
478             final byte[] bytes = EditorUiUtils.getCompressedThumbnailBitmapBytes(
479                     getContext(), photoUri);
480             if (bytes != null) {
481                 mPhotoValuesDelta.setPhoto(bytes);
482             }
483         } catch (FileNotFoundException e) {
484             elog("Failed to get bitmap from photo Uri");
485         }
486 
487         mPhotoView.setFullSizedPhoto(photoUri);
488     }
489 
unsetSuperPrimaryFromAllPhotos()490     private void unsetSuperPrimaryFromAllPhotos() {
491         final List<KindSectionData> kindSectionDataList =
492                 mKindSectionDataMap.get(Photo.CONTENT_ITEM_TYPE);
493         for (KindSectionData kindSectionData : kindSectionDataList) {
494             for (ValuesDelta valuesDelta : kindSectionData.getNonEmptyValuesDeltas()) {
495                 valuesDelta.setSuperPrimary(false);
496             }
497         }
498     }
499 
500     /**
501      * Pass through to {@link CompactPhotoEditorView#isWritablePhotoSet}.
502      */
isWritablePhotoSet()503     public boolean isWritablePhotoSet() {
504         return mPhotoView.isWritablePhotoSet();
505     }
506 
507     /**
508      * Get the raw contact ID for the CompactHeaderView photo.
509      */
getPhotoRawContactId()510     public long getPhotoRawContactId() {
511         return mPhotoRawContactId;
512     }
513 
getPrimaryNameEditorView()514     public StructuredNameEditorView getPrimaryNameEditorView() {
515         final CompactKindSectionView primaryNameKindSectionView = getPrimaryNameKindSectionView();
516         return primaryNameKindSectionView == null
517                 ? null : primaryNameKindSectionView.getPrimaryNameEditorView();
518     }
519 
520     /**
521      * Returns a data holder for every non-default/non-empty photo from each raw contact, whether
522      * the raw contact is writable or not.
523      */
getPhotos()524     public ArrayList<CompactPhotoSelectionFragment.Photo> getPhotos() {
525         final ArrayList<CompactPhotoSelectionFragment.Photo> photos = new ArrayList<>();
526 
527         final Bundle updatedPhotos = mListener == null ? null : mListener.getUpdatedPhotos();
528 
529         final List<KindSectionData> kindSectionDataList =
530                 mKindSectionDataMap.get(Photo.CONTENT_ITEM_TYPE);
531         for (int i = 0; i < kindSectionDataList.size(); i++) {
532             final KindSectionData kindSectionData = kindSectionDataList.get(i);
533             final AccountType accountType = kindSectionData.getAccountType();
534             final List<ValuesDelta> valuesDeltas = kindSectionData.getNonEmptyValuesDeltas();
535             if (valuesDeltas.isEmpty()) continue;
536             for (int j = 0; j < valuesDeltas.size(); j++) {
537                 final ValuesDelta valuesDelta = valuesDeltas.get(j);
538                 final Bitmap bitmap = EditorUiUtils.getPhotoBitmap(valuesDelta);
539                 if (bitmap == null) continue;
540 
541                 final CompactPhotoSelectionFragment.Photo photo =
542                         new CompactPhotoSelectionFragment.Photo();
543                 photo.titleRes = accountType.titleRes;
544                 photo.iconRes = accountType.iconRes;
545                 photo.syncAdapterPackageName = accountType.syncAdapterPackageName;
546                 photo.valuesDelta = valuesDelta;
547                 photo.primary = valuesDelta.isSuperPrimary();
548                 photo.kindSectionDataListIndex = i;
549                 photo.valuesDeltaListIndex = j;
550                 photo.photoId = valuesDelta.getId();
551 
552                 if (updatedPhotos != null) {
553                     photo.updatedPhotoUri = (Uri) updatedPhotos.get(String.valueOf(
554                             kindSectionData.getRawContactDelta().getRawContactId()));
555                 }
556 
557                 final CharSequence accountTypeLabel = accountType.getDisplayLabel(getContext());
558                 photo.accountType = accountTypeLabel == null ? "" : accountTypeLabel.toString();
559                 final String accountName = kindSectionData.getRawContactDelta().getAccountName();
560                 photo.accountName = accountName == null ? "" : accountName;
561 
562                 photos.add(photo);
563             }
564         }
565 
566         return photos;
567     }
568 
569     /**
570      * Marks the raw contact photo given as primary for the aggregate contact and updates the
571      * UI.
572      */
setPrimaryPhoto(CompactPhotoSelectionFragment.Photo photo)573     public void setPrimaryPhoto(CompactPhotoSelectionFragment.Photo photo) {
574         // Find the values delta to mark as primary
575         final KindSectionDataList kindSectionDataList =
576                 mKindSectionDataMap.get(Photo.CONTENT_ITEM_TYPE);
577         if (photo.kindSectionDataListIndex < 0
578                 || photo.kindSectionDataListIndex >= kindSectionDataList.size()) {
579             wlog("Invalid kind section data list index");
580             return;
581         }
582         final KindSectionData kindSectionData =
583                 kindSectionDataList.get(photo.kindSectionDataListIndex);
584         final List<ValuesDelta> valuesDeltaList = kindSectionData.getNonEmptyValuesDeltas();
585         if (photo.valuesDeltaListIndex >= valuesDeltaList.size()) {
586             wlog("Invalid values delta list index");
587             return;
588         }
589 
590         // Update values delta
591         final ValuesDelta valuesDelta = valuesDeltaList.get(photo.valuesDeltaListIndex);
592         valuesDelta.setFromTemplate(false);
593         unsetSuperPrimaryFromAllPhotos();
594         valuesDelta.setSuperPrimary(true);
595 
596         // Update the UI
597         mPhotoView.setPhoto(valuesDelta, mMaterialPalette);
598     }
599 
getAggregationAnchorView()600     public View getAggregationAnchorView() {
601         final List<CompactKindSectionView> kindSectionViews = getKindSectionViews(
602                 StructuredName.CONTENT_ITEM_TYPE);
603         if (!kindSectionViews.isEmpty()) {
604             return mKindSectionViews.getChildAt(0).findViewById(R.id.anchor_view);
605         }
606         return null;
607     }
608 
setGroupMetaData(Cursor groupMetaData)609     public void setGroupMetaData(Cursor groupMetaData) {
610         final List<CompactKindSectionView> kindSectionViews = getKindSectionViews(
611                 GroupMembership.CONTENT_ITEM_TYPE);
612         for (CompactKindSectionView kindSectionView : kindSectionViews) {
613             kindSectionView.setGroupMetaData(groupMetaData);
614             if (mIsExpanded) {
615                 kindSectionView.setHideWhenEmpty(false);
616                 kindSectionView.updateEmptyEditors(/* shouldAnimate =*/ true);
617             }
618         }
619     }
620 
setState(RawContactDeltaList rawContactDeltas, MaterialColorMapUtils.MaterialPalette materialPalette, ViewIdGenerator viewIdGenerator, long photoId, boolean hasNewContact, boolean isUserProfile, AccountWithDataSet primaryAccount)621     public void setState(RawContactDeltaList rawContactDeltas,
622             MaterialColorMapUtils.MaterialPalette materialPalette, ViewIdGenerator viewIdGenerator,
623             long photoId, boolean hasNewContact, boolean isUserProfile,
624             AccountWithDataSet primaryAccount) {
625         mKindSectionDataMap.clear();
626         mKindSectionViews.removeAllViews();
627         mMoreFields.setVisibility(View.VISIBLE);
628 
629         mMaterialPalette = materialPalette;
630         mViewIdGenerator = viewIdGenerator;
631         mPhotoId = photoId;
632 
633         mHasNewContact = hasNewContact;
634         mIsUserProfile = isUserProfile;
635         mPrimaryAccount = primaryAccount;
636         if (mPrimaryAccount == null) {
637             mPrimaryAccount = ContactEditorUtils.getInstance(getContext()).getDefaultAccount();
638         }
639         vlog("state: primary " + mPrimaryAccount);
640 
641         // Parse the given raw contact deltas
642         if (rawContactDeltas == null || rawContactDeltas.isEmpty()) {
643             elog("No raw contact deltas");
644             if (mListener != null) mListener.onBindEditorsFailed();
645             return;
646         }
647         parseRawContactDeltas(rawContactDeltas);
648         if (mKindSectionDataMap.isEmpty()) {
649             elog("No kind section data parsed from RawContactDelta(s)");
650             if (mListener != null) mListener.onBindEditorsFailed();
651             return;
652         }
653 
654         // Get the primary name kind section data
655         mPrimaryNameKindSectionData = mKindSectionDataMap.get(StructuredName.CONTENT_ITEM_TYPE)
656                 .getEntryToWrite(/* id =*/ -1, mPrimaryAccount, mIsUserProfile);
657         if (mPrimaryNameKindSectionData != null) {
658             // Ensure that a structured name and photo exists
659             final RawContactDelta rawContactDelta =
660                     mPrimaryNameKindSectionData.first.getRawContactDelta();
661             RawContactModifier.ensureKindExists(
662                     rawContactDelta,
663                     rawContactDelta.getAccountType(mAccountTypeManager),
664                     StructuredName.CONTENT_ITEM_TYPE);
665             RawContactModifier.ensureKindExists(
666                     rawContactDelta,
667                     rawContactDelta.getAccountType(mAccountTypeManager),
668                     Photo.CONTENT_ITEM_TYPE);
669         }
670 
671         // Setup the view
672         addAccountInfo(rawContactDeltas);
673         addPhotoView();
674         addKindSectionViews();
675 
676         if (mIsExpanded) showAllFields();
677 
678         if (mListener != null) mListener.onEditorsBound();
679     }
680 
parseRawContactDeltas(RawContactDeltaList rawContactDeltas)681     private void parseRawContactDeltas(RawContactDeltaList rawContactDeltas) {
682         // Build the kind section data list map
683         vlog("parse: " + rawContactDeltas.size() + " rawContactDelta(s)");
684         for (int j = 0; j < rawContactDeltas.size(); j++) {
685             final RawContactDelta rawContactDelta = rawContactDeltas.get(j);
686             vlog("parse: " + j + " rawContactDelta" + rawContactDelta);
687             if (rawContactDelta == null || !rawContactDelta.isVisible()) continue;
688             final AccountType accountType = rawContactDelta.getAccountType(mAccountTypeManager);
689             if (accountType == null) continue;
690             final List<DataKind> dataKinds = accountType.getSortedDataKinds();
691             final int dataKindSize = dataKinds == null ? 0 : dataKinds.size();
692             vlog("parse: " + dataKindSize + " dataKinds(s)");
693             for (int i = 0; i < dataKindSize; i++) {
694                 final DataKind dataKind = dataKinds.get(i);
695                 if (dataKind == null || !dataKind.editable) {
696                     vlog("parse: " + i + " " + dataKind.mimeType + " dropped read-only");
697                     continue;
698                 }
699                 final String mimeType = dataKind.mimeType;
700 
701                 // Skip psuedo mime types
702                 if (DataKind.PSEUDO_MIME_TYPE_DISPLAY_NAME.equals(mimeType)
703                         || DataKind.PSEUDO_MIME_TYPE_PHONETIC_NAME.equals(mimeType)) {
704                     vlog("parse: " + i + " " + dataKind.mimeType + " dropped pseudo type");
705                     continue;
706                 }
707 
708                 final KindSectionDataList kindSectionDataList =
709                         getOrCreateKindSectionDataList(mimeType);
710                 final KindSectionData kindSectionData =
711                         new KindSectionData(accountType, dataKind, rawContactDelta);
712                 kindSectionDataList.add(kindSectionData);
713 
714                 vlog("parse: " + i + " " + dataKind.mimeType + " " +
715                         kindSectionData.getValuesDeltas().size() + " value(s) " +
716                         kindSectionData.getNonEmptyValuesDeltas().size() + " non-empty value(s) " +
717                         kindSectionData.getVisibleValuesDeltas().size() +
718                         " visible value(s)");
719             }
720         }
721     }
722 
getOrCreateKindSectionDataList(String mimeType)723     private KindSectionDataList getOrCreateKindSectionDataList(String mimeType) {
724         KindSectionDataList kindSectionDataList = mKindSectionDataMap.get(mimeType);
725         if (kindSectionDataList == null) {
726             kindSectionDataList = new KindSectionDataList();
727             mKindSectionDataMap.put(mimeType, kindSectionDataList);
728         }
729         return kindSectionDataList;
730     }
731 
addAccountInfo(RawContactDeltaList rawContactDeltas)732     private void addAccountInfo(RawContactDeltaList rawContactDeltas) {
733         mAccountHeaderContainer.setVisibility(View.GONE);
734         mAccountSelectorContainer.setVisibility(View.GONE);
735         mRawContactContainer.setVisibility(View.GONE);
736 
737         if (mPrimaryNameKindSectionData == null) return;
738         final RawContactDelta rawContactDelta =
739                 mPrimaryNameKindSectionData.first.getRawContactDelta();
740 
741         // Get the account information for the primary raw contact delta
742         final Pair<String,String> accountInfo = mIsUserProfile
743                 ? EditorUiUtils.getLocalAccountInfo(getContext(),
744                         rawContactDelta.getAccountName(),
745                         rawContactDelta.getAccountType(mAccountTypeManager))
746                 : EditorUiUtils.getAccountInfo(getContext(),
747                         rawContactDelta.getAccountName(),
748                         rawContactDelta.getAccountType(mAccountTypeManager));
749 
750         // Either the account header or selector should be shown, not both.
751         final List<AccountWithDataSet> accounts =
752                 AccountTypeManager.getInstance(getContext()).getAccounts(true);
753         if (mHasNewContact && !mIsUserProfile) {
754             if (accounts.size() > 1) {
755                 addAccountSelector(accountInfo, rawContactDelta);
756             } else {
757                 addAccountHeader(accountInfo);
758             }
759         } else if (mIsUserProfile || !shouldHideAccountContainer(rawContactDeltas)) {
760             addAccountHeader(accountInfo);
761         }
762 
763         // The raw contact selector should only display linked raw contacts that can be edited in
764         // the full editor (i.e. they are not newly created raw contacts)
765         final RawContactAccountListAdapter adapter =  new RawContactAccountListAdapter(getContext(),
766                 getRawContactDeltaListForSelector(rawContactDeltas));
767         if (adapter.getCount() > 0) {
768             final String accountsSummary = getResources().getQuantityString(
769                     R.plurals.compact_editor_linked_contacts_selector_title,
770                     adapter.getCount(), adapter.getCount());
771             addRawContactAccountSelector(accountsSummary, adapter);
772         }
773     }
774 
getRawContactDeltaListForSelector( RawContactDeltaList rawContactDeltas)775     private RawContactDeltaList getRawContactDeltaListForSelector(
776             RawContactDeltaList rawContactDeltas) {
777         // Sort raw contacts so google accounts come first
778         Collections.sort(rawContactDeltas, new RawContactDeltaComparator(getContext()));
779 
780         final RawContactDeltaList result = new RawContactDeltaList();
781         for (RawContactDelta rawContactDelta : rawContactDeltas) {
782             if (rawContactDelta.isVisible() && rawContactDelta.getRawContactId() > 0) {
783                 // Only add raw contacts that can be opened in the editor
784                 result.add(rawContactDelta);
785             }
786         }
787         // Don't return a list of size 1 that would just open the raw contact being edited
788         // in the compact editor in the full editor
789         if (result.size() == 1 && result.get(0).getRawContactAccountType(
790                 getContext()).areContactsWritable()) {
791             result.clear();
792             return result;
793         }
794         return result;
795     }
796 
797     // Returns true if there are multiple writable rawcontacts and no read-only ones,
798     // or there are both writable and read-only rawcontacts.
shouldHideAccountContainer(RawContactDeltaList rawContactDeltas)799     private boolean shouldHideAccountContainer(RawContactDeltaList rawContactDeltas) {
800         int writable = 0;
801         int readonly = 0;
802         for (RawContactDelta rawContactDelta : rawContactDeltas) {
803             if (rawContactDelta.isVisible() && rawContactDelta.getRawContactId() > 0) {
804                 if (rawContactDelta.getRawContactAccountType(getContext()).areContactsWritable()) {
805                     writable++;
806                 } else {
807                     readonly++;
808                 }
809             }
810         }
811         return (writable > 1 || (writable > 0 && readonly > 0));
812     }
813 
addAccountHeader(Pair<String,String> accountInfo)814     private void addAccountHeader(Pair<String,String> accountInfo) {
815         mAccountHeaderContainer.setVisibility(View.VISIBLE);
816 
817         // Set the account name
818         final String accountName = TextUtils.isEmpty(accountInfo.first)
819                 ? accountInfo.second : accountInfo.first;
820         mAccountHeaderName.setVisibility(View.VISIBLE);
821         mAccountHeaderName.setText(accountName);
822 
823         // Set the account type
824         final String selectorTitle = getResources().getString(
825                 R.string.compact_editor_account_selector_title);
826         mAccountHeaderType.setText(selectorTitle);
827 
828         // Set the icon
829         if (mPrimaryNameKindSectionData != null) {
830             final RawContactDelta rawContactDelta =
831                     mPrimaryNameKindSectionData.first.getRawContactDelta();
832             if (rawContactDelta != null) {
833                 final AccountType accountType =
834                         rawContactDelta.getRawContactAccountType(getContext());
835                 mAccountHeaderIcon.setImageDrawable(accountType.getDisplayIcon(getContext()));
836             }
837         }
838 
839         // Set the content description
840         mAccountHeaderContainer.setContentDescription(
841                 EditorUiUtils.getAccountInfoContentDescription(accountName, selectorTitle));
842     }
843 
addAccountSelector(Pair<String,String> accountInfo, final RawContactDelta rawContactDelta)844     private void addAccountSelector(Pair<String,String> accountInfo,
845             final RawContactDelta rawContactDelta) {
846         mAccountSelectorContainer.setVisibility(View.VISIBLE);
847 
848         if (TextUtils.isEmpty(accountInfo.first)) {
849             // Hide this view so the other text view will be centered vertically
850             mAccountSelectorName.setVisibility(View.GONE);
851         } else {
852             mAccountSelectorName.setVisibility(View.VISIBLE);
853             mAccountSelectorName.setText(accountInfo.first);
854         }
855 
856         final String selectorTitle = getResources().getString(
857                 R.string.compact_editor_account_selector_title);
858         mAccountSelectorType.setText(selectorTitle);
859 
860         mAccountSelectorContainer.setContentDescription(getResources().getString(
861                 R.string.compact_editor_account_selector_description, accountInfo.first));
862 
863         mAccountSelectorContainer.setOnClickListener(new View.OnClickListener() {
864             @Override
865             public void onClick(View v) {
866                 final ListPopupWindow popup = new ListPopupWindow(getContext(), null);
867                 final AccountsListAdapter adapter =
868                         new AccountsListAdapter(getContext(),
869                                 AccountsListAdapter.AccountListFilter.ACCOUNTS_CONTACT_WRITABLE,
870                                 mPrimaryAccount);
871                 popup.setWidth(mAccountSelectorContainer.getWidth());
872                 popup.setAnchorView(mAccountSelectorContainer);
873                 popup.setAdapter(adapter);
874                 popup.setModal(true);
875                 popup.setInputMethodMode(ListPopupWindow.INPUT_METHOD_NOT_NEEDED);
876                 popup.setOnItemClickListener(new AdapterView.OnItemClickListener() {
877                     @Override
878                     public void onItemClick(AdapterView<?> parent, View view, int position,
879                             long id) {
880                         UiClosables.closeQuietly(popup);
881                         final AccountWithDataSet newAccount = adapter.getItem(position);
882                         if (mListener != null && !mPrimaryAccount.equals(newAccount)) {
883                             mListener.onRebindEditorsForNewContact(
884                                     rawContactDelta,
885                                     mPrimaryAccount,
886                                     newAccount);
887                         }
888                     }
889                 });
890                 popup.show();
891             }
892         });
893     }
894 
addRawContactAccountSelector(String accountsSummary, final RawContactAccountListAdapter adapter)895     private void addRawContactAccountSelector(String accountsSummary,
896             final RawContactAccountListAdapter adapter) {
897         mRawContactContainer.setVisibility(View.VISIBLE);
898 
899         mRawContactSummary.setText(accountsSummary);
900 
901         mRawContactContainer.setOnClickListener(new View.OnClickListener() {
902             @Override
903             public void onClick(View v) {
904                 final ListPopupWindow popup = new ListPopupWindow(getContext(), null);
905                 popup.setWidth(mRawContactContainer.getWidth());
906                 popup.setAnchorView(mRawContactContainer);
907                 popup.setAdapter(adapter);
908                 popup.setModal(true);
909                 popup.setInputMethodMode(ListPopupWindow.INPUT_METHOD_NOT_NEEDED);
910                 popup.setOnItemClickListener(new AdapterView.OnItemClickListener() {
911                     @Override
912                     public void onItemClick(AdapterView<?> parent, View view, int position,
913                                             long id) {
914                         UiClosables.closeQuietly(popup);
915 
916                         if (mListener != null) {
917                             final long rawContactId = adapter.getItemId(position);
918                             final Uri rawContactUri = ContentUris.withAppendedId(
919                                     ContactsContract.RawContacts.CONTENT_URI, rawContactId);
920                             final RawContactDelta rawContactDelta = adapter.getItem(position);
921                             final AccountTypeManager accountTypes = AccountTypeManager.getInstance(
922                                     getContext());
923                             final AccountType accountType = rawContactDelta.getAccountType(
924                                     accountTypes);
925                             final boolean isReadOnly = !accountType.areContactsWritable();
926 
927                             mListener.onRawContactSelected(rawContactUri, rawContactId, isReadOnly);
928                         }
929                     }
930                 });
931                 popup.show();
932             }
933         });
934     }
935 
addPhotoView()936     private void addPhotoView() {
937         // Get the kind section data and values delta that we will display in the photo view
938         final KindSectionDataList kindSectionDataList =
939                 mKindSectionDataMap.get(Photo.CONTENT_ITEM_TYPE);
940         final Pair<KindSectionData,ValuesDelta> photoToDisplay =
941                 kindSectionDataList.getEntryToDisplay(mPhotoId);
942         if (photoToDisplay == null) {
943             wlog("photo: no kind section data parsed");
944             mPhotoView.setVisibility(View.GONE);
945             return;
946         }
947 
948         // Set the photo view
949         mPhotoView.setPhoto(photoToDisplay.second, mMaterialPalette);
950 
951         // Find the raw contact ID and values delta that will be written when the photo is edited
952         final Pair<KindSectionData, ValuesDelta> photoToWrite = kindSectionDataList.getEntryToWrite(
953                 mPhotoId, mPrimaryAccount, mIsUserProfile);
954         if (photoToWrite == null) {
955             mPhotoView.setReadOnly(true);
956             return;
957         }
958         mPhotoView.setReadOnly(false);
959         mPhotoRawContactId = photoToWrite.first.getRawContactDelta().getRawContactId();
960         mPhotoValuesDelta = photoToWrite.second;
961     }
962 
addKindSectionViews()963     private void addKindSectionViews() {
964         // Sort the kinds
965         final TreeSet<Map.Entry<String,KindSectionDataList>> entries =
966                 new TreeSet<>(KIND_SECTION_DATA_MAP_ENTRY_COMPARATOR);
967         entries.addAll(mKindSectionDataMap.entrySet());
968 
969         vlog("kind: " + entries.size() + " kindSection(s)");
970         int i = -1;
971         for (Map.Entry<String, KindSectionDataList> entry : entries) {
972             i++;
973 
974             final String mimeType = entry.getKey();
975 
976             if (StructuredName.CONTENT_ITEM_TYPE.equals(mimeType)) {
977                 if (mPrimaryNameKindSectionData == null) {
978                     vlog("kind: " + i + " " + mimeType + " dropped");
979                     continue;
980                 }
981                 vlog("kind: " + i + " " + mimeType + " using first entry only");
982                 final KindSectionDataList kindSectionDataList = new KindSectionDataList();
983                 kindSectionDataList.add(mPrimaryNameKindSectionData.first);
984                 final CompactKindSectionView kindSectionView = inflateKindSectionView(
985                         mKindSectionViews, kindSectionDataList, mimeType,
986                         mPrimaryNameKindSectionData.second);
987                 mKindSectionViews.addView(kindSectionView);
988 
989                 // Keep a pointer to all the KindSectionsViews for each mimeType
990                 getKindSectionViews(mimeType).add(kindSectionView);
991             } else {
992                 final KindSectionDataList kindSectionDataList = entry.getValue();
993 
994                 // Ignore mime types that we've already handled
995                 if (Photo.CONTENT_ITEM_TYPE.equals(mimeType)) {
996                     vlog("kind: " + i + " " + mimeType + " dropped");
997                     continue;
998                 }
999 
1000                 // Don't show more than one group editor on the compact editor.
1001                 // Groups will still be editable for each raw contact individually on the full editor.
1002                 if (GroupMembership.CONTENT_ITEM_TYPE.equals(mimeType)
1003                         && kindSectionDataList.size() > 1) {
1004                     vlog("kind: " + i + " " + mimeType + " dropped");
1005                     continue;
1006                 }
1007 
1008                 if (kindSectionDataList != null && !kindSectionDataList.isEmpty()) {
1009                     vlog("kind: " + i + " " + mimeType + " " + kindSectionDataList.size() +
1010                             " kindSectionData(s)");
1011 
1012                     final CompactKindSectionView kindSectionView = inflateKindSectionView(
1013                             mKindSectionViews, kindSectionDataList, mimeType,
1014                             /* primaryValueDelta =*/ null);
1015                     mKindSectionViews.addView(kindSectionView);
1016 
1017                     // Keep a pointer to all the KindSectionsViews for each mimeType
1018                     getKindSectionViews(mimeType).add(kindSectionView);
1019                 }
1020             }
1021         }
1022     }
1023 
getKindSectionViews(String mimeType)1024     private List<CompactKindSectionView> getKindSectionViews(String mimeType) {
1025         List<CompactKindSectionView> kindSectionViews = mKindSectionViewsMap.get(mimeType);
1026         if (kindSectionViews == null) {
1027             kindSectionViews = new ArrayList<>();
1028             mKindSectionViewsMap.put(mimeType, kindSectionViews);
1029         }
1030         return kindSectionViews;
1031     }
1032 
inflateKindSectionView(ViewGroup viewGroup, KindSectionDataList kindSectionDataList, String mimeType, ValuesDelta primaryValuesDelta)1033     private CompactKindSectionView inflateKindSectionView(ViewGroup viewGroup,
1034             KindSectionDataList kindSectionDataList, String mimeType,
1035             ValuesDelta primaryValuesDelta) {
1036         final CompactKindSectionView kindSectionView = (CompactKindSectionView)
1037                 mLayoutInflater.inflate(R.layout.compact_item_kind_section, viewGroup,
1038                         /* attachToRoot =*/ false);
1039         kindSectionView.setIsUserProfile(mIsUserProfile);
1040 
1041         if (Phone.CONTENT_ITEM_TYPE.equals(mimeType)
1042                 || Email.CONTENT_ITEM_TYPE.equals(mimeType)) {
1043             // Phone numbers and email addresses are always displayed,
1044             // even if they are empty
1045             kindSectionView.setHideWhenEmpty(false);
1046         }
1047 
1048         // Since phone numbers and email addresses displayed even if they are empty,
1049         // they will be the only types you add new values to initially for new contacts
1050         kindSectionView.setShowOneEmptyEditor(true);
1051 
1052         // Sort non-name editors so they wind up in the order we want
1053         if (!StructuredName.CONTENT_ITEM_TYPE.equals(mimeType)) {
1054             Collections.sort(kindSectionDataList, new EditorComparator(getContext()));
1055         }
1056 
1057         kindSectionView.setState(kindSectionDataList, mViewIdGenerator, mListener,
1058                 primaryValuesDelta);
1059 
1060         return kindSectionView;
1061     }
1062 
maybeSetReadOnlyDisplayNameAsPrimary(String readOnlyDisplayName)1063     void maybeSetReadOnlyDisplayNameAsPrimary(String readOnlyDisplayName) {
1064         if (TextUtils.isEmpty(readOnlyDisplayName)) return;
1065         final CompactKindSectionView primaryNameKindSectionView = getPrimaryNameKindSectionView();
1066         if (primaryNameKindSectionView != null && primaryNameKindSectionView.isEmptyName()) {
1067             vlog("name: using read only display name as primary name");
1068             primaryNameKindSectionView.setName(readOnlyDisplayName);
1069         }
1070     }
1071 
getPrimaryNameKindSectionView()1072     private CompactKindSectionView getPrimaryNameKindSectionView() {
1073         final List<CompactKindSectionView> kindSectionViews
1074                 = mKindSectionViewsMap.get(StructuredName.CONTENT_ITEM_TYPE);
1075         return kindSectionViews == null || kindSectionViews.isEmpty()
1076                 ? null : kindSectionViews.get(0);
1077     }
1078 
showAllFields()1079     private void showAllFields() {
1080         // Stop hiding empty editors and allow the user to enter values for all kinds now
1081         for (int i = 0; i < mKindSectionViews.getChildCount(); i++) {
1082             final CompactKindSectionView kindSectionView =
1083                     (CompactKindSectionView) mKindSectionViews.getChildAt(i);
1084             kindSectionView.setHideWhenEmpty(false);
1085             kindSectionView.updateEmptyEditors(/* shouldAnimate =*/ true);
1086         }
1087         mIsExpanded = true;
1088 
1089         // Hide the more fields button
1090         mMoreFields.setVisibility(View.GONE);
1091     }
1092 
vlog(String message)1093     private static void vlog(String message) {
1094         if (Log.isLoggable(TAG, Log.VERBOSE)) {
1095             Log.v(TAG, message);
1096         }
1097     }
1098 
wlog(String message)1099     private static void wlog(String message) {
1100         if (Log.isLoggable(TAG, Log.WARN)) {
1101             Log.w(TAG, message);
1102         }
1103     }
1104 
elog(String message)1105     private static void elog(String message) {
1106         Log.e(TAG, message);
1107     }
1108 }
1109