• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2015 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.contacts.editor;
18 
19 import android.content.Context;
20 import android.database.Cursor;
21 import android.provider.ContactsContract.CommonDataKinds.Event;
22 import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
23 import android.provider.ContactsContract.CommonDataKinds.Nickname;
24 import android.provider.ContactsContract.CommonDataKinds.StructuredName;
25 import android.util.AttributeSet;
26 import android.view.LayoutInflater;
27 import android.view.View;
28 import android.view.ViewGroup;
29 import android.widget.ImageView;
30 import android.widget.LinearLayout;
31 import android.widget.TextView;
32 
33 import com.android.contacts.R;
34 import com.android.contacts.model.RawContactDelta;
35 import com.android.contacts.model.RawContactModifier;
36 import com.android.contacts.model.ValuesDelta;
37 import com.android.contacts.model.account.AccountType;
38 import com.android.contacts.model.dataitem.DataKind;
39 import com.android.contacts.preference.ContactsPreferences;
40 
41 import java.util.ArrayList;
42 import java.util.List;
43 
44 /**
45  * Custom view for an entire section of data as segmented by
46  * {@link DataKind} around a {@link Data#MIMETYPE}. This view shows a
47  * section header and a trigger for adding new {@link Data} rows.
48  */
49 public class KindSectionView extends LinearLayout {
50 
51     /**
52      * Marks a name as super primary when it is changed.
53      *
54      * This is for the case when two or more raw contacts with names are joined where neither is
55      * marked as super primary.
56      */
57     private static final class StructuredNameEditorListener implements Editor.EditorListener {
58 
59         private final ValuesDelta mValuesDelta;
60         private final long mRawContactId;
61         private final RawContactEditorView.Listener mListener;
62 
StructuredNameEditorListener(ValuesDelta valuesDelta, long rawContactId, RawContactEditorView.Listener listener)63         public StructuredNameEditorListener(ValuesDelta valuesDelta, long rawContactId,
64                 RawContactEditorView.Listener listener) {
65             mValuesDelta = valuesDelta;
66             mRawContactId = rawContactId;
67             mListener = listener;
68         }
69 
70         @Override
onRequest(int request)71         public void onRequest(int request) {
72             if (request == Editor.EditorListener.FIELD_CHANGED) {
73                 mValuesDelta.setSuperPrimary(true);
74                 if (mListener != null) {
75                     mListener.onNameFieldChanged(mRawContactId, mValuesDelta);
76                 }
77             } else if (request == Editor.EditorListener.FIELD_TURNED_EMPTY) {
78                 mValuesDelta.setSuperPrimary(false);
79             }
80         }
81 
82         @Override
onDeleteRequested(Editor editor)83         public void onDeleteRequested(Editor editor) {
84             editor.clearAllFields();
85         }
86     }
87 
88     /**
89      * Clears fields when deletes are requested (on phonetic and nickename fields);
90      * does not change the number of editors.
91      */
92     private static final class OtherNameKindEditorListener implements Editor.EditorListener {
93 
94         @Override
onRequest(int request)95         public void onRequest(int request) {
96         }
97 
98         @Override
onDeleteRequested(Editor editor)99         public void onDeleteRequested(Editor editor) {
100             editor.clearAllFields();
101         }
102     }
103 
104     /**
105      * Updates empty fields when fields are deleted or turns empty.
106      * Whether a new empty editor is added is controlled by {@link #setShowOneEmptyEditor} and
107      * {@link #setHideWhenEmpty}.
108      */
109     private class NonNameEditorListener implements Editor.EditorListener {
110 
111         @Override
onRequest(int request)112         public void onRequest(int request) {
113             // If a field has become empty or non-empty, then check if another row
114             // can be added dynamically.
115             if (request == FIELD_TURNED_EMPTY || request == FIELD_TURNED_NON_EMPTY) {
116                 updateEmptyEditors(/* shouldAnimate = */ true);
117             }
118         }
119 
120         @Override
onDeleteRequested(Editor editor)121         public void onDeleteRequested(Editor editor) {
122             if (mShowOneEmptyEditor && mEditors.getChildCount() == 1) {
123                 // If there is only 1 editor in the section, then don't allow the user to
124                 // delete it.  Just clear the fields in the editor.
125                 editor.clearAllFields();
126             } else {
127                 editor.deleteEditor();
128             }
129         }
130     }
131 
132     private class EventEditorListener extends NonNameEditorListener {
133 
134         @Override
onRequest(int request)135         public void onRequest(int request) {
136             super.onRequest(request);
137         }
138 
139         @Override
onDeleteRequested(Editor editor)140         public void onDeleteRequested(Editor editor) {
141             if (editor instanceof EventFieldEditorView){
142                 final EventFieldEditorView delView = (EventFieldEditorView) editor;
143                 if (delView.isBirthdayType() && mEditors.getChildCount() > 1) {
144                     final EventFieldEditorView bottomView = (EventFieldEditorView) mEditors
145                             .getChildAt(mEditors.getChildCount() - 1);
146                     bottomView.restoreBirthday();
147                 }
148             }
149             super.onDeleteRequested(editor);
150         }
151     }
152 
153     private KindSectionData mKindSectionData;
154     private ViewIdGenerator mViewIdGenerator;
155     private RawContactEditorView.Listener mListener;
156 
157     private boolean mIsUserProfile;
158     private boolean mShowOneEmptyEditor = false;
159     private boolean mHideIfEmpty = true;
160 
161     private LayoutInflater mLayoutInflater;
162     private ViewGroup mEditors;
163     private ImageView mIcon;
164 
KindSectionView(Context context)165     public KindSectionView(Context context) {
166         this(context, /* attrs =*/ null);
167     }
168 
KindSectionView(Context context, AttributeSet attrs)169     public KindSectionView(Context context, AttributeSet attrs) {
170         super(context, attrs);
171     }
172 
173     @Override
setEnabled(boolean enabled)174     public void setEnabled(boolean enabled) {
175         super.setEnabled(enabled);
176         if (mEditors != null) {
177             int childCount = mEditors.getChildCount();
178             for (int i = 0; i < childCount; i++) {
179                 mEditors.getChildAt(i).setEnabled(enabled);
180             }
181         }
182     }
183 
184     @Override
onFinishInflate()185     protected void onFinishInflate() {
186         super.onFinishInflate();
187         setDrawingCacheEnabled(true);
188         setAlwaysDrawnWithCacheEnabled(true);
189 
190         mLayoutInflater = (LayoutInflater) getContext().getSystemService(
191                 Context.LAYOUT_INFLATER_SERVICE);
192 
193         mEditors = (ViewGroup) findViewById(R.id.kind_editors);
194         mIcon = (ImageView) findViewById(R.id.kind_icon);
195     }
196 
setIsUserProfile(boolean isUserProfile)197     public void setIsUserProfile(boolean isUserProfile) {
198         mIsUserProfile = isUserProfile;
199     }
200 
201     /**
202      * @param showOneEmptyEditor If true, we will always show one empty editor, otherwise an empty
203      *         editor will not be shown until the user enters a value.  Note, this does not apply
204      *         to name editors since those are always displayed.
205      */
setShowOneEmptyEditor(boolean showOneEmptyEditor)206     public void setShowOneEmptyEditor(boolean showOneEmptyEditor) {
207         mShowOneEmptyEditor = showOneEmptyEditor;
208     }
209 
210     /**
211      * @param hideWhenEmpty If true, the entire section will be hidden if all inputs are empty,
212      *         otherwise one empty input will always be displayed.  Note, this does not apply
213      *         to name editors since those are always displayed.
214      */
setHideWhenEmpty(boolean hideWhenEmpty)215     public void setHideWhenEmpty(boolean hideWhenEmpty) {
216         mHideIfEmpty = hideWhenEmpty;
217     }
218 
219     /** Binds the given group data to every {@link GroupMembershipView}. */
setGroupMetaData(Cursor cursor)220     public void setGroupMetaData(Cursor cursor) {
221         for (int i = 0; i < mEditors.getChildCount(); i++) {
222             final View view = mEditors.getChildAt(i);
223             if (view instanceof GroupMembershipView) {
224                 ((GroupMembershipView) view).setGroupMetaData(cursor);
225             }
226         }
227     }
228 
229     /**
230      * Whether this is a name kind section view and all name fields (structured, phonetic,
231      * and nicknames) are empty.
232      */
isEmptyName()233     public boolean isEmptyName() {
234         if (!StructuredName.CONTENT_ITEM_TYPE.equals(mKindSectionData.getMimeType())) {
235             return false;
236         }
237         for (int i = 0; i < mEditors.getChildCount(); i++) {
238             final View view = mEditors.getChildAt(i);
239             if (view instanceof Editor) {
240                 final Editor editor = (Editor) view;
241                 if (!editor.isEmpty()) {
242                     return false;
243                 }
244             }
245         }
246         return true;
247     }
248 
getNameEditorView()249     public StructuredNameEditorView getNameEditorView() {
250         if (!StructuredName.CONTENT_ITEM_TYPE.equals(mKindSectionData.getMimeType())
251             || mEditors.getChildCount() == 0) {
252             return null;
253         }
254         return (StructuredNameEditorView) mEditors.getChildAt(0);
255     }
256 
257     /**
258      * Binds views for the given {@link KindSectionData}.
259      *
260      * We create a structured name and phonetic name editor for each {@link DataKind} with a
261      * {@link StructuredName#CONTENT_ITEM_TYPE} mime type.  The number and order of editors are
262      * rendered as they are given to {@link #setState}.
263      *
264      * Empty name editors are never added and at least one structured name editor is always
265      * displayed, even if it is empty.
266      */
setState(KindSectionData kindSectionData, ViewIdGenerator viewIdGenerator, RawContactEditorView.Listener listener)267     public void setState(KindSectionData kindSectionData,
268             ViewIdGenerator viewIdGenerator, RawContactEditorView.Listener listener) {
269         mKindSectionData = kindSectionData;
270         mViewIdGenerator = viewIdGenerator;
271         mListener = listener;
272 
273         // Set the icon using the DataKind
274         final DataKind dataKind = mKindSectionData.getDataKind();
275         if (dataKind != null) {
276             mIcon.setImageDrawable(EditorUiUtils.getMimeTypeDrawable(getContext(),
277                     dataKind.mimeType));
278             if (mIcon.getDrawable() != null) {
279                 mIcon.setContentDescription(dataKind.titleRes == -1 || dataKind.titleRes == 0
280                         ? "" : getResources().getString(dataKind.titleRes));
281             }
282         }
283 
284         rebuildFromState();
285 
286         updateEmptyEditors(/* shouldAnimate = */ false);
287     }
288 
rebuildFromState()289     private void rebuildFromState() {
290         mEditors.removeAllViews();
291 
292         final String mimeType = mKindSectionData.getMimeType();
293         if (StructuredName.CONTENT_ITEM_TYPE.equals(mimeType)) {
294             addNameEditorViews(mKindSectionData.getAccountType(),
295                     mKindSectionData.getRawContactDelta());
296         } else if (GroupMembership.CONTENT_ITEM_TYPE.equals(mimeType)) {
297             addGroupEditorView(mKindSectionData.getRawContactDelta(),
298                     mKindSectionData.getDataKind());
299         } else {
300             final Editor.EditorListener editorListener;
301             if (Nickname.CONTENT_ITEM_TYPE.equals(mimeType)) {
302                 editorListener = new OtherNameKindEditorListener();
303             } else if (Event.CONTENT_ITEM_TYPE.equals(mimeType)) {
304                 editorListener = new EventEditorListener();
305             } else {
306                 editorListener = new NonNameEditorListener();
307             }
308             final List<ValuesDelta> valuesDeltas = mKindSectionData.getVisibleValuesDeltas();
309             for (int i = 0; i < valuesDeltas.size(); i++ ) {
310                 addNonNameEditorView(mKindSectionData.getRawContactDelta(),
311                         mKindSectionData.getDataKind(), valuesDeltas.get(i), editorListener);
312             }
313         }
314     }
315 
addNameEditorViews(AccountType accountType, RawContactDelta rawContactDelta)316     private void addNameEditorViews(AccountType accountType, RawContactDelta rawContactDelta) {
317         final boolean readOnly = !accountType.areContactsWritable();
318         final ValuesDelta nameValuesDelta = rawContactDelta
319                 .getSuperPrimaryEntry(StructuredName.CONTENT_ITEM_TYPE);
320 
321         if (readOnly) {
322             final View nameView = mLayoutInflater.inflate(
323                     R.layout.structured_name_readonly_editor_view, mEditors,
324                     /* attachToRoot =*/ false);
325 
326             // Display name
327             ((TextView) nameView.findViewById(R.id.display_name))
328                     .setText(nameValuesDelta.getDisplayName());
329 
330             // Account type info
331             final LinearLayout accountTypeLayout = (LinearLayout)
332                     nameView.findViewById(R.id.account_type);
333             accountTypeLayout.setVisibility(View.VISIBLE);
334             ((ImageView) accountTypeLayout.findViewById(R.id.account_type_icon))
335                     .setImageDrawable(accountType.getDisplayIcon(getContext()));
336             ((TextView) accountTypeLayout.findViewById(R.id.account_type_name))
337                     .setText(accountType.getDisplayLabel(getContext()));
338 
339             mEditors.addView(nameView);
340             return;
341         }
342 
343         // Structured name
344         final StructuredNameEditorView nameView = (StructuredNameEditorView) mLayoutInflater
345                 .inflate(R.layout.structured_name_editor_view, mEditors, /* attachToRoot =*/ false);
346         if (!mIsUserProfile) {
347             // Don't set super primary for the me contact
348             nameView.setEditorListener(new StructuredNameEditorListener(
349                     nameValuesDelta, rawContactDelta.getRawContactId(), mListener));
350         }
351         nameView.setDeletable(false);
352         nameView.setValues(accountType.getKindForMimetype(DataKind.PSEUDO_MIME_TYPE_NAME),
353                 nameValuesDelta, rawContactDelta, /* readOnly =*/ false, mViewIdGenerator);
354 
355         // Correct start margin since there is a second icon in the structured name layout
356         nameView.findViewById(R.id.kind_icon).setVisibility(View.GONE);
357         mEditors.addView(nameView);
358 
359         // Phonetic name
360         final DataKind phoneticNameKind = accountType
361                 .getKindForMimetype(DataKind.PSEUDO_MIME_TYPE_PHONETIC_NAME);
362         // The account type doesn't support phonetic name.
363         if (phoneticNameKind == null) return;
364 
365         final TextFieldsEditorView phoneticNameView = (TextFieldsEditorView) mLayoutInflater
366                 .inflate(R.layout.text_fields_editor_view, mEditors, /* attachToRoot =*/ false);
367         phoneticNameView.setEditorListener(new OtherNameKindEditorListener());
368         phoneticNameView.setDeletable(false);
369         phoneticNameView.setValues(
370                 accountType.getKindForMimetype(DataKind.PSEUDO_MIME_TYPE_PHONETIC_NAME),
371                 nameValuesDelta, rawContactDelta, /* readOnly =*/ false, mViewIdGenerator);
372 
373         // Fix the start margin for phonetic name views
374         final LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(
375                 LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
376         layoutParams.setMargins(0, 0, 0, 0);
377         phoneticNameView.setLayoutParams(layoutParams);
378         mEditors.addView(phoneticNameView);
379         // Display of phonetic name fields is controlled from settings preferences.
380         mHideIfEmpty = new ContactsPreferences(getContext()).shouldHidePhoneticNamesIfEmpty();
381     }
382 
addGroupEditorView(RawContactDelta rawContactDelta, DataKind dataKind)383     private void addGroupEditorView(RawContactDelta rawContactDelta, DataKind dataKind) {
384         final GroupMembershipView view = (GroupMembershipView) mLayoutInflater.inflate(
385                 R.layout.item_group_membership, mEditors, /* attachToRoot =*/ false);
386         view.setKind(dataKind);
387         view.setEnabled(isEnabled());
388         view.setState(rawContactDelta);
389 
390         // Correct start margin since there is a second icon in the group layout
391         view.findViewById(R.id.kind_icon).setVisibility(View.GONE);
392 
393         mEditors.addView(view);
394     }
395 
addNonNameEditorView(RawContactDelta rawContactDelta, DataKind dataKind, ValuesDelta valuesDelta, Editor.EditorListener editorListener)396     private View addNonNameEditorView(RawContactDelta rawContactDelta, DataKind dataKind,
397             ValuesDelta valuesDelta, Editor.EditorListener editorListener) {
398         // Inflate the layout
399         final View view = mLayoutInflater.inflate(
400                 EditorUiUtils.getLayoutResourceId(dataKind.mimeType), mEditors, false);
401         view.setEnabled(isEnabled());
402         if (view instanceof Editor) {
403             final Editor editor = (Editor) view;
404             editor.setDeletable(true);
405             editor.setEditorListener(editorListener);
406             editor.setValues(dataKind, valuesDelta, rawContactDelta, !dataKind.editable,
407                     mViewIdGenerator);
408         }
409         mEditors.addView(view);
410 
411         return view;
412     }
413 
414     /**
415      * Updates the editors being displayed to the user removing extra empty
416      * {@link Editor}s, so there is only max 1 empty {@link Editor} view at a time.
417      * If there is only 1 empty editor and {@link #setHideWhenEmpty} was set to true,
418      * then the entire section is hidden.
419      */
updateEmptyEditors(boolean shouldAnimate)420     public void updateEmptyEditors(boolean shouldAnimate) {
421         final boolean isNameKindSection = StructuredName.CONTENT_ITEM_TYPE.equals(
422                 mKindSectionData.getMimeType());
423         final boolean isGroupKindSection = GroupMembership.CONTENT_ITEM_TYPE.equals(
424                 mKindSectionData.getMimeType());
425 
426         if (isNameKindSection) {
427             // The name kind section is always visible
428             setVisibility(VISIBLE);
429             updateEmptyNameEditors(shouldAnimate);
430         } else if (isGroupKindSection) {
431             // Check whether metadata has been bound for all group views
432             for (int i = 0; i < mEditors.getChildCount(); i++) {
433                 final View view = mEditors.getChildAt(i);
434                 if (view instanceof GroupMembershipView) {
435                     final GroupMembershipView groupView = (GroupMembershipView) view;
436                     if (!groupView.wasGroupMetaDataBound() || !groupView.accountHasGroups()) {
437                         setVisibility(GONE);
438                         return;
439                     }
440                 }
441             }
442             // Check that the user has selected to display all fields
443             if (mHideIfEmpty) {
444                 setVisibility(GONE);
445                 return;
446             }
447             setVisibility(VISIBLE);
448 
449             // We don't check the emptiness of the group views
450         } else {
451             // Determine if the entire kind section should be visible
452             final int editorCount = mEditors.getChildCount();
453             final List<View> emptyEditors = getEmptyEditors();
454             if (editorCount == emptyEditors.size() && mHideIfEmpty) {
455                 setVisibility(GONE);
456                 return;
457             }
458             setVisibility(VISIBLE);
459 
460             updateEmptyNonNameEditors(shouldAnimate);
461         }
462     }
463 
updateEmptyNameEditors(boolean shouldAnimate)464     private void updateEmptyNameEditors(boolean shouldAnimate) {
465         boolean isEmptyNameEditorVisible = false;
466 
467         for (int i = 0; i < mEditors.getChildCount(); i++) {
468             final View view = mEditors.getChildAt(i);
469             if (view instanceof Editor) {
470                 final Editor editor = (Editor) view;
471                 if (view instanceof StructuredNameEditorView) {
472                     // We always show one empty structured name view
473                     if (editor.isEmpty()) {
474                         if (isEmptyNameEditorVisible) {
475                             // If we're already showing an empty editor then hide any other empties
476                             if (mHideIfEmpty) {
477                                 view.setVisibility(View.GONE);
478                             }
479                         } else {
480                             isEmptyNameEditorVisible = true;
481                         }
482                     } else {
483                         showView(view, shouldAnimate);
484                         isEmptyNameEditorVisible = true;
485                     }
486                 } else {
487                     // Since we can't add phonetic names and nicknames, just show or hide them
488                     if (mHideIfEmpty && editor.isEmpty()) {
489                         hideView(view);
490                     } else {
491                         showView(view, /* shouldAnimate =*/ false); // Animation here causes jank
492                     }
493                 }
494             } else {
495                 // For read only names, only show them if we're not hiding empty views
496                 if (mHideIfEmpty) {
497                     hideView(view);
498                 } else {
499                     showView(view, shouldAnimate);
500                 }
501             }
502         }
503     }
504 
updateEmptyNonNameEditors(boolean shouldAnimate)505     private void updateEmptyNonNameEditors(boolean shouldAnimate) {
506         // Prune excess empty editors
507         final List<View> emptyEditors = getEmptyEditors();
508         if (emptyEditors.size() > 1) {
509             // If there is more than 1 empty editor, then remove it from the list of editors.
510             int deleted = 0;
511             for (int i = 0; i < emptyEditors.size(); i++) {
512                 final View view = emptyEditors.get(i);
513                 // If no child {@link View}s are being focused on within this {@link View}, then
514                 // remove this empty editor. We can assume that at least one empty editor has
515                 // focus. One way to get two empty editors is by deleting characters from a
516                 // non-empty editor, in which case this editor has focus.  Another way is if
517                 // there is more values delta so we must also count number of editors deleted.
518                 if (view.findFocus() == null) {
519                     deleteView(view, shouldAnimate);
520                     deleted++;
521                     if (deleted == emptyEditors.size() - 1) break;
522                 }
523             }
524             return;
525         }
526         // Determine if we should add a new empty editor
527         final DataKind dataKind = mKindSectionData.getDataKind();
528         final RawContactDelta rawContactDelta = mKindSectionData.getRawContactDelta();
529         if (dataKind == null // There is nothing we can do.
530                 // We have already reached the maximum number of editors, don't add any more.
531                 || !RawContactModifier.canInsert(rawContactDelta, dataKind)
532                 // We have already reached the maximum number of empty editors, don't add any more.
533                 || emptyEditors.size() == 1) {
534             return;
535         }
536         // Add a new empty editor
537         if (mShowOneEmptyEditor) {
538             final String mimeType = mKindSectionData.getMimeType();
539             if (Nickname.CONTENT_ITEM_TYPE.equals(mimeType) && mEditors.getChildCount() > 0) {
540                 return;
541             }
542             final ValuesDelta values = RawContactModifier.insertChild(rawContactDelta, dataKind);
543             final Editor.EditorListener editorListener = Event.CONTENT_ITEM_TYPE.equals(mimeType)
544                     ? new EventEditorListener() : new NonNameEditorListener();
545             final View view = addNonNameEditorView(rawContactDelta, dataKind, values,
546                     editorListener);
547             showView(view, shouldAnimate);
548         }
549     }
550 
hideView(View view)551     private void hideView(View view) {
552         view.setVisibility(View.GONE);
553     }
554 
deleteView(View view, boolean shouldAnimate)555     private void deleteView(View view, boolean shouldAnimate) {
556         if (shouldAnimate) {
557             final Editor editor = (Editor) view;
558             editor.deleteEditor();
559         } else {
560             mEditors.removeView(view);
561         }
562     }
563 
showView(View view, boolean shouldAnimate)564     private void showView(View view, boolean shouldAnimate) {
565         if (shouldAnimate) {
566             view.setVisibility(View.GONE);
567             EditorAnimator.getInstance().showFieldFooter(view);
568         } else {
569             view.setVisibility(View.VISIBLE);
570         }
571     }
572 
getEmptyEditors()573     private List<View> getEmptyEditors() {
574         final List<View> emptyEditors = new ArrayList<>();
575         for (int i = 0; i < mEditors.getChildCount(); i++) {
576             final View view = mEditors.getChildAt(i);
577             if (view instanceof Editor && ((Editor) view).isEmpty()) {
578                 emptyEditors.add(view);
579             }
580         }
581         return emptyEditors;
582     }
583 }
584