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.RawContactDelta;
21 import com.android.contacts.common.model.RawContactModifier;
22 import com.android.contacts.common.model.ValuesDelta;
23 import com.android.contacts.common.model.account.AccountType;
24 import com.android.contacts.common.model.dataitem.DataKind;
25 
26 import android.content.Context;
27 import android.database.Cursor;
28 import android.provider.ContactsContract.CommonDataKinds.Event;
29 import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
30 import android.provider.ContactsContract.CommonDataKinds.Nickname;
31 import android.provider.ContactsContract.CommonDataKinds.StructuredName;
32 import android.util.AttributeSet;
33 import android.view.LayoutInflater;
34 import android.view.View;
35 import android.view.ViewGroup;
36 import android.widget.ImageView;
37 import android.widget.LinearLayout;
38 import android.widget.TextView;
39 
40 import java.util.ArrayList;
41 import java.util.List;
42 
43 /**
44  * Version of {@link KindSectionView} that supports multiple RawContactDeltas.
45  */
46 public 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 
StructuredNameEditorListener(ValuesDelta valuesDelta, long rawContactId, CompactRawContactsEditorView.Listener listener)60         public StructuredNameEditorListener(ValuesDelta valuesDelta, long rawContactId,
61                 CompactRawContactsEditorView.Listener listener) {
62             mValuesDelta = valuesDelta;
63             mRawContactId = rawContactId;
64             mListener = listener;
65         }
66 
67         @Override
onRequest(int request)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
onDeleteRequested(Editor editor)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
onRequest(int request)92         public void onRequest(int request) {
93         }
94 
95         @Override
onDeleteRequested(Editor editor)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
onRequest(int request)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
onDeleteRequested(Editor editor)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
onRequest(int request)132         public void onRequest(int request) {
133             super.onRequest(request);
134         }
135 
136         @Override
onDeleteRequested(Editor editor)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 
CompactKindSectionView(Context context)162     public CompactKindSectionView(Context context) {
163         this(context, /* attrs =*/ null);
164     }
165 
CompactKindSectionView(Context context, AttributeSet attrs)166     public CompactKindSectionView(Context context, AttributeSet attrs) {
167         super(context, attrs);
168     }
169 
170     @Override
setEnabled(boolean enabled)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
onFinishInflate()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 
setIsUserProfile(boolean isUserProfile)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      */
setShowOneEmptyEditor(boolean showOneEmptyEditor)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      */
setHideWhenEmpty(boolean hideWhenEmpty)211     public void setHideWhenEmpty(boolean hideWhenEmpty) {
212         mHideIfEmpty = hideWhenEmpty;
213     }
214 
215     /** Binds the given group data to every {@link GroupMembershipView}. */
setGroupMetaData(Cursor cursor)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      */
isEmptyName()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      */
setName(String displayName)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 
getPrimaryNameEditorView()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      */
setState(KindSectionDataList kindSectionDataList, ViewIdGenerator viewIdGenerator, CompactRawContactsEditorView.Listener listener, ValuesDelta primaryValuesDelta)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 
rebuildFromState(ValuesDelta primaryValuesDelta)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 
addNameEditorViews(AccountType accountType, ValuesDelta valuesDelta, RawContactDelta rawContactDelta)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 
addGroupEditorView(RawContactDelta rawContactDelta, DataKind dataKind)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 
addNonNameEditorView(RawContactDelta rawContactDelta, DataKind dataKind, ValuesDelta valuesDelta, Editor.EditorListener editorListener)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      */
updateEmptyEditors(boolean shouldAnimate)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 
updateEmptyNameEditors(boolean shouldAnimate)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 
updateEmptyNonNameEditors(boolean shouldAnimate)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 
hideView(View view)569     private void hideView(View view) {
570         view.setVisibility(View.GONE);
571     }
572 
deleteView(View view, boolean shouldAnimate)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 
showView(View view, boolean shouldAnimate)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 
getEmptyEditors()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