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