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 android.content.Context;
20import android.database.Cursor;
21import android.provider.ContactsContract.CommonDataKinds.Event;
22import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
23import android.provider.ContactsContract.CommonDataKinds.Nickname;
24import android.provider.ContactsContract.CommonDataKinds.StructuredName;
25import android.util.AttributeSet;
26import android.view.LayoutInflater;
27import android.view.View;
28import android.view.ViewGroup;
29import android.widget.ImageView;
30import android.widget.LinearLayout;
31import android.widget.TextView;
32
33import com.android.contacts.R;
34import com.android.contacts.model.RawContactDelta;
35import com.android.contacts.model.RawContactModifier;
36import com.android.contacts.model.ValuesDelta;
37import com.android.contacts.model.account.AccountType;
38import com.android.contacts.model.dataitem.DataKind;
39import com.android.contacts.preference.ContactsPreferences;
40
41import java.util.ArrayList;
42import 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 */
49public 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
63        public StructuredNameEditorListener(ValuesDelta valuesDelta, long rawContactId,
64                RawContactEditorView.Listener listener) {
65            mValuesDelta = valuesDelta;
66            mRawContactId = rawContactId;
67            mListener = listener;
68        }
69
70        @Override
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
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
95        public void onRequest(int request) {
96        }
97
98        @Override
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
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
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
135        public void onRequest(int request) {
136            super.onRequest(request);
137        }
138
139        @Override
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
165    public KindSectionView(Context context) {
166        this(context, /* attrs =*/ null);
167    }
168
169    public KindSectionView(Context context, AttributeSet attrs) {
170        super(context, attrs);
171    }
172
173    @Override
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
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
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     */
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     */
215    public void setHideWhenEmpty(boolean hideWhenEmpty) {
216        mHideIfEmpty = hideWhenEmpty;
217    }
218
219    /** Binds the given group data to every {@link GroupMembershipView}. */
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     */
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
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     */
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
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
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
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
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     */
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
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
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
551    private void hideView(View view) {
552        view.setVisibility(View.GONE);
553    }
554
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
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
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