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 
getPhoneticEditorView()257     public TextFieldsEditorView getPhoneticEditorView() {
258         if (!StructuredName.CONTENT_ITEM_TYPE.equals(mKindSectionData.getMimeType())) {
259             return null;
260         }
261         for (int i = 0; i < mEditors.getChildCount(); i++) {
262             final View view = mEditors.getChildAt(i);
263             if (!(view instanceof StructuredNameEditorView)) {
264                 return (TextFieldsEditorView) view;
265             }
266         }
267         return null;
268     }
269 
270     /**
271      * Binds views for the given {@link KindSectionData}.
272      *
273      * We create a structured name and phonetic name editor for each {@link DataKind} with a
274      * {@link StructuredName#CONTENT_ITEM_TYPE} mime type.  The number and order of editors are
275      * rendered as they are given to {@link #setState}.
276      *
277      * Empty name editors are never added and at least one structured name editor is always
278      * displayed, even if it is empty.
279      */
setState(KindSectionData kindSectionData, ViewIdGenerator viewIdGenerator, RawContactEditorView.Listener listener)280     public void setState(KindSectionData kindSectionData,
281             ViewIdGenerator viewIdGenerator, RawContactEditorView.Listener listener) {
282         mKindSectionData = kindSectionData;
283         mViewIdGenerator = viewIdGenerator;
284         mListener = listener;
285 
286         // Set the icon using the DataKind
287         final DataKind dataKind = mKindSectionData.getDataKind();
288         if (dataKind != null) {
289             mIcon.setImageDrawable(EditorUiUtils.getMimeTypeDrawable(getContext(),
290                     dataKind.mimeType));
291             if (mIcon.getDrawable() != null) {
292                 mIcon.setContentDescription(dataKind.titleRes == -1 || dataKind.titleRes == 0
293                         ? "" : getResources().getString(dataKind.titleRes));
294             }
295         }
296 
297         rebuildFromState();
298 
299         updateEmptyEditors(/* shouldAnimate = */ false);
300     }
301 
rebuildFromState()302     private void rebuildFromState() {
303         mEditors.removeAllViews();
304 
305         final String mimeType = mKindSectionData.getMimeType();
306         if (StructuredName.CONTENT_ITEM_TYPE.equals(mimeType)) {
307             addNameEditorViews(mKindSectionData.getAccountType(),
308                     mKindSectionData.getRawContactDelta());
309         } else if (GroupMembership.CONTENT_ITEM_TYPE.equals(mimeType)) {
310             addGroupEditorView(mKindSectionData.getRawContactDelta(),
311                     mKindSectionData.getDataKind());
312         } else {
313             final Editor.EditorListener editorListener;
314             if (Nickname.CONTENT_ITEM_TYPE.equals(mimeType)) {
315                 editorListener = new OtherNameKindEditorListener();
316             } else if (Event.CONTENT_ITEM_TYPE.equals(mimeType)) {
317                 editorListener = new EventEditorListener();
318             } else {
319                 editorListener = new NonNameEditorListener();
320             }
321             final List<ValuesDelta> valuesDeltas = mKindSectionData.getVisibleValuesDeltas();
322             for (int i = 0; i < valuesDeltas.size(); i++ ) {
323                 addNonNameEditorView(mKindSectionData.getRawContactDelta(),
324                         mKindSectionData.getDataKind(), valuesDeltas.get(i), editorListener);
325             }
326         }
327     }
328 
addNameEditorViews(AccountType accountType, RawContactDelta rawContactDelta)329     private void addNameEditorViews(AccountType accountType, RawContactDelta rawContactDelta) {
330         final boolean readOnly = !accountType.areContactsWritable();
331         final ValuesDelta nameValuesDelta = rawContactDelta
332                 .getSuperPrimaryEntry(StructuredName.CONTENT_ITEM_TYPE);
333 
334         if (readOnly) {
335             final View nameView = mLayoutInflater.inflate(
336                     R.layout.structured_name_readonly_editor_view, mEditors,
337                     /* attachToRoot =*/ false);
338 
339             // Display name
340             ((TextView) nameView.findViewById(R.id.display_name))
341                     .setText(nameValuesDelta.getDisplayName());
342 
343             // Account type info
344             final LinearLayout accountTypeLayout = (LinearLayout)
345                     nameView.findViewById(R.id.account_type);
346             accountTypeLayout.setVisibility(View.VISIBLE);
347             ((ImageView) accountTypeLayout.findViewById(R.id.account_type_icon))
348                     .setImageDrawable(accountType.getDisplayIcon(getContext()));
349             ((TextView) accountTypeLayout.findViewById(R.id.account_type_name))
350                     .setText(accountType.getDisplayLabel(getContext()));
351 
352             mEditors.addView(nameView);
353             return;
354         }
355 
356         // Structured name
357         final StructuredNameEditorView nameView = (StructuredNameEditorView) mLayoutInflater
358                 .inflate(R.layout.structured_name_editor_view, mEditors, /* attachToRoot =*/ false);
359         if (!mIsUserProfile) {
360             // Don't set super primary for the me contact
361             nameView.setEditorListener(new StructuredNameEditorListener(
362                     nameValuesDelta, rawContactDelta.getRawContactId(), mListener));
363         }
364         nameView.setDeletable(false);
365         nameView.setValues(accountType.getKindForMimetype(DataKind.PSEUDO_MIME_TYPE_NAME),
366                 nameValuesDelta, rawContactDelta, /* readOnly =*/ false, mViewIdGenerator);
367 
368         // Correct start margin since there is a second icon in the structured name layout
369         nameView.findViewById(R.id.kind_icon).setVisibility(View.GONE);
370         mEditors.addView(nameView);
371 
372         // Phonetic name
373         final DataKind phoneticNameKind = accountType
374                 .getKindForMimetype(DataKind.PSEUDO_MIME_TYPE_PHONETIC_NAME);
375         // The account type doesn't support phonetic name.
376         if (phoneticNameKind == null) return;
377 
378         final TextFieldsEditorView phoneticNameView = (TextFieldsEditorView) mLayoutInflater
379                 .inflate(R.layout.text_fields_editor_view, mEditors, /* attachToRoot =*/ false);
380         phoneticNameView.setEditorListener(new OtherNameKindEditorListener());
381         phoneticNameView.setDeletable(false);
382         phoneticNameView.setValues(
383                 accountType.getKindForMimetype(DataKind.PSEUDO_MIME_TYPE_PHONETIC_NAME),
384                 nameValuesDelta, rawContactDelta, /* readOnly =*/ false, mViewIdGenerator);
385 
386         // Fix the start margin for phonetic name views
387         final LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(
388                 LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
389         layoutParams.setMargins(0, 0, 0, 0);
390         phoneticNameView.setLayoutParams(layoutParams);
391         mEditors.addView(phoneticNameView);
392         // Display of phonetic name fields is controlled from settings preferences.
393         mHideIfEmpty = new ContactsPreferences(getContext()).shouldHidePhoneticNamesIfEmpty();
394     }
395 
addGroupEditorView(RawContactDelta rawContactDelta, DataKind dataKind)396     private void addGroupEditorView(RawContactDelta rawContactDelta, DataKind dataKind) {
397         final GroupMembershipView view = (GroupMembershipView) mLayoutInflater.inflate(
398                 R.layout.item_group_membership, mEditors, /* attachToRoot =*/ false);
399         view.setKind(dataKind);
400         view.setEnabled(isEnabled());
401         view.setState(rawContactDelta);
402 
403         // Correct start margin since there is a second icon in the group layout
404         view.findViewById(R.id.kind_icon).setVisibility(View.GONE);
405 
406         mEditors.addView(view);
407     }
408 
addNonNameEditorView(RawContactDelta rawContactDelta, DataKind dataKind, ValuesDelta valuesDelta, Editor.EditorListener editorListener)409     private View addNonNameEditorView(RawContactDelta rawContactDelta, DataKind dataKind,
410             ValuesDelta valuesDelta, Editor.EditorListener editorListener) {
411         // Inflate the layout
412         final View view = mLayoutInflater.inflate(
413                 EditorUiUtils.getLayoutResourceId(dataKind.mimeType), mEditors, false);
414         view.setEnabled(isEnabled());
415         if (view instanceof Editor) {
416             final Editor editor = (Editor) view;
417             editor.setDeletable(true);
418             editor.setEditorListener(editorListener);
419             editor.setValues(dataKind, valuesDelta, rawContactDelta, !dataKind.editable,
420                     mViewIdGenerator);
421         }
422         mEditors.addView(view);
423 
424         return view;
425     }
426 
427     /**
428      * Updates the editors being displayed to the user removing extra empty
429      * {@link Editor}s, so there is only max 1 empty {@link Editor} view at a time.
430      * If there is only 1 empty editor and {@link #setHideWhenEmpty} was set to true,
431      * then the entire section is hidden.
432      */
updateEmptyEditors(boolean shouldAnimate)433     public void updateEmptyEditors(boolean shouldAnimate) {
434         final boolean isNameKindSection = StructuredName.CONTENT_ITEM_TYPE.equals(
435                 mKindSectionData.getMimeType());
436         final boolean isGroupKindSection = GroupMembership.CONTENT_ITEM_TYPE.equals(
437                 mKindSectionData.getMimeType());
438 
439         if (isNameKindSection) {
440             // The name kind section is always visible
441             setVisibility(VISIBLE);
442             updateEmptyNameEditors(shouldAnimate);
443         } else if (isGroupKindSection) {
444             // Check whether metadata has been bound for all group views
445             for (int i = 0; i < mEditors.getChildCount(); i++) {
446                 final View view = mEditors.getChildAt(i);
447                 if (view instanceof GroupMembershipView) {
448                     final GroupMembershipView groupView = (GroupMembershipView) view;
449                     if (!groupView.wasGroupMetaDataBound() || !groupView.accountHasGroups()) {
450                         setVisibility(GONE);
451                         return;
452                     }
453                 }
454             }
455             // Check that the user has selected to display all fields
456             if (mHideIfEmpty) {
457                 setVisibility(GONE);
458                 return;
459             }
460             setVisibility(VISIBLE);
461 
462             // We don't check the emptiness of the group views
463         } else {
464             // Determine if the entire kind section should be visible
465             final int editorCount = mEditors.getChildCount();
466             final List<View> emptyEditors = getEmptyEditors();
467             if (editorCount == emptyEditors.size() && mHideIfEmpty) {
468                 setVisibility(GONE);
469                 return;
470             }
471             setVisibility(VISIBLE);
472 
473             updateEmptyNonNameEditors(shouldAnimate);
474         }
475     }
476 
updateEmptyNameEditors(boolean shouldAnimate)477     private void updateEmptyNameEditors(boolean shouldAnimate) {
478         boolean isEmptyNameEditorVisible = false;
479 
480         for (int i = 0; i < mEditors.getChildCount(); i++) {
481             final View view = mEditors.getChildAt(i);
482             if (view instanceof Editor) {
483                 final Editor editor = (Editor) view;
484                 if (view instanceof StructuredNameEditorView) {
485                     // We always show one empty structured name view
486                     if (editor.isEmpty()) {
487                         if (isEmptyNameEditorVisible) {
488                             // If we're already showing an empty editor then hide any other empties
489                             if (mHideIfEmpty) {
490                                 view.setVisibility(View.GONE);
491                             }
492                         } else {
493                             isEmptyNameEditorVisible = true;
494                         }
495                     } else {
496                         showView(view, shouldAnimate);
497                         isEmptyNameEditorVisible = true;
498                     }
499                 } else {
500                     // Since we can't add phonetic names and nicknames, just show or hide them
501                     if (mHideIfEmpty && editor.isEmpty()) {
502                         hideView(view);
503                     } else {
504                         showView(view, /* shouldAnimate =*/ false); // Animation here causes jank
505                     }
506                 }
507             } else {
508                 // For read only names, only show them if we're not hiding empty views
509                 if (mHideIfEmpty) {
510                     hideView(view);
511                 } else {
512                     showView(view, shouldAnimate);
513                 }
514             }
515         }
516     }
517 
updateEmptyNonNameEditors(boolean shouldAnimate)518     private void updateEmptyNonNameEditors(boolean shouldAnimate) {
519         // Prune excess empty editors
520         final List<View> emptyEditors = getEmptyEditors();
521         if (emptyEditors.size() > 1) {
522             // If there is more than 1 empty editor, then remove it from the list of editors.
523             int deleted = 0;
524             for (int i = 0; i < emptyEditors.size(); i++) {
525                 final View view = emptyEditors.get(i);
526                 // If no child {@link View}s are being focused on within this {@link View}, then
527                 // remove this empty editor. We can assume that at least one empty editor has
528                 // focus. One way to get two empty editors is by deleting characters from a
529                 // non-empty editor, in which case this editor has focus.  Another way is if
530                 // there is more values delta so we must also count number of editors deleted.
531                 if (view.findFocus() == null) {
532                     deleteView(view, shouldAnimate);
533                     deleted++;
534                     if (deleted == emptyEditors.size() - 1) break;
535                 }
536             }
537             return;
538         }
539         // Determine if we should add a new empty editor
540         final DataKind dataKind = mKindSectionData.getDataKind();
541         final RawContactDelta rawContactDelta = mKindSectionData.getRawContactDelta();
542         if (dataKind == null // There is nothing we can do.
543                 // We have already reached the maximum number of editors, don't add any more.
544                 || !RawContactModifier.canInsert(rawContactDelta, dataKind)
545                 // We have already reached the maximum number of empty editors, don't add any more.
546                 || emptyEditors.size() == 1) {
547             return;
548         }
549         // Add a new empty editor
550         if (mShowOneEmptyEditor) {
551             final String mimeType = mKindSectionData.getMimeType();
552             if (Nickname.CONTENT_ITEM_TYPE.equals(mimeType) && mEditors.getChildCount() > 0) {
553                 return;
554             }
555             final ValuesDelta values = RawContactModifier.insertChild(rawContactDelta, dataKind);
556             final Editor.EditorListener editorListener = Event.CONTENT_ITEM_TYPE.equals(mimeType)
557                     ? new EventEditorListener() : new NonNameEditorListener();
558             final View view = addNonNameEditorView(rawContactDelta, dataKind, values,
559                     editorListener);
560             showView(view, shouldAnimate);
561         }
562     }
563 
hideView(View view)564     private void hideView(View view) {
565         view.setVisibility(View.GONE);
566     }
567 
deleteView(View view, boolean shouldAnimate)568     private void deleteView(View view, boolean shouldAnimate) {
569         if (shouldAnimate) {
570             final Editor editor = (Editor) view;
571             editor.deleteEditor();
572         } else {
573             mEditors.removeView(view);
574         }
575     }
576 
showView(View view, boolean shouldAnimate)577     private void showView(View view, boolean shouldAnimate) {
578         if (shouldAnimate) {
579             view.setVisibility(View.GONE);
580             EditorAnimator.getInstance().showFieldFooter(view);
581         } else {
582             view.setVisibility(View.VISIBLE);
583         }
584     }
585 
getEmptyEditors()586     private List<View> getEmptyEditors() {
587         final List<View> emptyEditors = new ArrayList<>();
588         for (int i = 0; i < mEditors.getChildCount(); i++) {
589             final View view = mEditors.getChildAt(i);
590             if (view instanceof Editor && ((Editor) view).isEmpty()) {
591                 emptyEditors.add(view);
592             }
593         }
594         return emptyEditors;
595     }
596 }
597