• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2009 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.graphics.drawable.Drawable;
21 import android.provider.Contacts.GroupMembership;
22 import android.provider.ContactsContract.CommonDataKinds.Email;
23 import android.provider.ContactsContract.CommonDataKinds.Event;
24 import android.provider.ContactsContract.CommonDataKinds.Im;
25 import android.provider.ContactsContract.CommonDataKinds.Note;
26 import android.provider.ContactsContract.CommonDataKinds.Organization;
27 import android.provider.ContactsContract.CommonDataKinds.Phone;
28 import android.provider.ContactsContract.CommonDataKinds.Photo;
29 import android.provider.ContactsContract.CommonDataKinds.Relation;
30 import android.provider.ContactsContract.CommonDataKinds.SipAddress;
31 import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
32 import android.provider.ContactsContract.CommonDataKinds.Website;
33 import android.provider.ContactsContract.Data;
34 import android.text.TextUtils;
35 import android.util.AttributeSet;
36 import android.view.LayoutInflater;
37 import android.view.View;
38 import android.view.ViewGroup;
39 import android.widget.ImageView;
40 import android.widget.LinearLayout;
41 
42 import com.android.contacts.R;
43 import com.android.contacts.common.model.RawContactDelta;
44 import com.android.contacts.common.model.RawContactModifier;
45 import com.android.contacts.common.model.ValuesDelta;
46 import com.android.contacts.common.model.dataitem.DataKind;
47 import com.android.contacts.editor.Editor.EditorListener;
48 
49 import java.util.ArrayList;
50 import java.util.List;
51 
52 /**
53  * Custom view for an entire section of data as segmented by
54  * {@link DataKind} around a {@link Data#MIMETYPE}. This view shows a
55  * section header and a trigger for adding new {@link Data} rows.
56  */
57 public class KindSectionView extends LinearLayout implements EditorListener {
58 
59     public interface Listener {
60 
61         /**
62          * Invoked when any editor that is displayed in this section view is deleted by the user.
63          */
onDeleteRequested(Editor editor)64         public void onDeleteRequested(Editor editor);
65     }
66 
67     private ViewGroup mEditors;
68     private ImageView mIcon;
69 
70     private DataKind mKind;
71     private RawContactDelta mState;
72     private boolean mReadOnly;
73     private boolean mShowOneEmptyEditor;
74 
75     /**
76      * Whether this KindSectionView will be removed from the layout.
77      * We need this because we want to animate KindSectionViews away (which takes time),
78      * but calculate which KindSectionViews will be visible immediately after starting removal
79      * animations.
80      */
81     private boolean mMarkedForRemoval;
82 
83     private ViewIdGenerator mViewIdGenerator;
84 
85     private LayoutInflater mInflater;
86 
87     private Listener mListener;
88 
KindSectionView(Context context)89     public KindSectionView(Context context) {
90         this(context, null);
91     }
92 
KindSectionView(Context context, AttributeSet attrs)93     public KindSectionView(Context context, AttributeSet attrs) {
94         super(context, attrs);
95     }
96 
97     @Override
setEnabled(boolean enabled)98     public void setEnabled(boolean enabled) {
99         super.setEnabled(enabled);
100         if (mEditors != null) {
101             int childCount = mEditors.getChildCount();
102             for (int i = 0; i < childCount; i++) {
103                 mEditors.getChildAt(i).setEnabled(enabled);
104             }
105         }
106 
107         updateEmptyEditors(/* shouldAnimate = */ true);
108     }
109 
isReadOnly()110     public boolean isReadOnly() {
111         return mReadOnly;
112     }
113 
114     /** {@inheritDoc} */
115     @Override
onFinishInflate()116     protected void onFinishInflate() {
117         setDrawingCacheEnabled(true);
118         setAlwaysDrawnWithCacheEnabled(true);
119 
120         mInflater = (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
121 
122         mEditors = (ViewGroup) findViewById(R.id.kind_editors);
123         mIcon = (ImageView) findViewById(R.id.kind_icon);
124     }
125 
126     @Override
onDeleteRequested(Editor editor)127     public void onDeleteRequested(Editor editor) {
128         if (mShowOneEmptyEditor && getEditorCount() == 1) {
129             // If there is only 1 editor in the section, then don't allow the user to delete it.
130             // Just clear the fields in the editor.
131             editor.clearAllFields();
132         } else {
133             // If there is a listener, let it decide whether to delete the Editor or the entire
134             // KindSectionView so that there is no jank from both animations happening in succession.
135             if (mListener != null) {
136                 editor.markDeleted();
137                 mListener.onDeleteRequested(editor);
138             } else {
139                 editor.deleteEditor();
140             }
141         }
142     }
143 
144     /**
145      * Calling this signifies that this entire section view is intended to be removed from the
146      * layout. Note, calling this does not change the deleted state of any underlying
147      * {@link Editor}, i.e. {@link com.android.contacts.common.model.ValuesDelta#markDeleted()}
148      * is not invoked on any editor in this section.  It is purely marked for higher level UI
149      * layers to manipulate the layout w/o introducing jank.
150      * See b/22228718 for context.
151      */
markForRemoval()152     public void markForRemoval() {
153         mMarkedForRemoval = true;
154     }
155 
156     /**
157      * Whether the entire section view is intended to be removed from the layout.
158      */
isMarkedForRemoval()159     public boolean isMarkedForRemoval() {
160         return mMarkedForRemoval;
161     }
162 
163     @Override
onRequest(int request)164     public void onRequest(int request) {
165         // If a field has become empty or non-empty, then check if another row
166         // can be added dynamically.
167         if (request == FIELD_TURNED_EMPTY || request == FIELD_TURNED_NON_EMPTY) {
168             updateEmptyEditors(/* shouldAnimate = */ true);
169         }
170     }
171 
172     /**
173      * @param showOneEmptyEditor If true, one empty input will always be displayed,
174      *         otherwise an empty input will only be displayed if there is no non-empty value.
175      */
setShowOneEmptyEditor(boolean showOneEmptyEditor)176     public void setShowOneEmptyEditor(boolean showOneEmptyEditor) {
177         mShowOneEmptyEditor = showOneEmptyEditor;
178     }
179 
setListener(Listener listener)180     public void setListener(Listener listener) {
181         mListener = listener;
182     }
183 
setIconVisibility(boolean visible)184     public void setIconVisibility(boolean visible) {
185         mIcon.setVisibility(visible ? View.VISIBLE : View.INVISIBLE);
186     }
187 
setState(DataKind kind, RawContactDelta state, boolean readOnly, ViewIdGenerator vig)188     public void setState(DataKind kind, RawContactDelta state, boolean readOnly,
189             ViewIdGenerator vig) {
190         mKind = kind;
191         mState = state;
192         mReadOnly = readOnly;
193         mViewIdGenerator = vig;
194 
195         setId(mViewIdGenerator.getId(state, kind, null, ViewIdGenerator.NO_VIEW_INDEX));
196 
197         // TODO: handle resources from remote packages
198         final String titleString = (kind.titleRes == -1 || kind.titleRes == 0)
199                 ? ""
200                 : getResources().getString(kind.titleRes);
201         mIcon.setContentDescription(titleString);
202 
203         mIcon.setImageDrawable(getMimeTypeDrawable(kind.mimeType));
204         if (mIcon.getDrawable() == null) {
205             mIcon.setContentDescription(null);
206         }
207 
208         rebuildFromState();
209         updateEmptyEditors(/* shouldAnimate = */ false);
210     }
211 
212     /**
213      * Build editors for all current {@link #mState} rows.
214      */
rebuildFromState()215     private void rebuildFromState() {
216         // Remove any existing editors
217         mEditors.removeAllViews();
218 
219         // Check if we are displaying anything here
220         boolean hasEntries = mState.hasMimeEntries(mKind.mimeType);
221 
222         if (hasEntries) {
223             for (ValuesDelta entry : mState.getMimeEntries(mKind.mimeType)) {
224                 // Skip entries that aren't visible
225                 if (!entry.isVisible()) continue;
226                 if (isEmptyNoop(entry)) continue;
227 
228                 createEditorView(entry);
229             }
230         }
231     }
232 
233 
234     /**
235      * Creates an EditorView for the given entry. This function must be used while constructing
236      * the views corresponding to the the object-model. The resulting EditorView is also added
237      * to the end of mEditors
238      */
createEditorView(ValuesDelta entry)239     private View createEditorView(ValuesDelta entry) {
240         final View view;
241         final int layoutResId = EditorUiUtils.getLayoutResourceId(mKind.mimeType);
242         try {
243             view = mInflater.inflate(layoutResId, mEditors, false);
244         } catch (Exception e) {
245             throw new RuntimeException(
246                     "Cannot allocate editor with layout resource ID " +
247                     layoutResId + " for MIME type " + mKind.mimeType +
248                     " with error " + e.toString());
249         }
250         // Hide the types drop downs until the associated edit field is focused
251         if (view instanceof LabeledEditorView) {
252             ((LabeledEditorView) view).setHideTypeInitially(true);
253         }
254 
255         view.setEnabled(isEnabled());
256 
257         if (view instanceof Editor) {
258             Editor editor = (Editor) view;
259             editor.setDeletable(true);
260             editor.setValues(mKind, entry, mState, mReadOnly, mViewIdGenerator);
261             editor.setEditorListener(this);
262         }
263         mEditors.addView(view);
264         return view;
265     }
266 
267     /**
268      * Tests whether the given item has no changes (so it exists in the database) but is empty
269      */
isEmptyNoop(ValuesDelta item)270     private boolean isEmptyNoop(ValuesDelta item) {
271         if (!item.isNoop()) return false;
272         final int fieldCount = mKind.fieldList.size();
273         for (int i = 0; i < fieldCount; i++) {
274             final String column = mKind.fieldList.get(i).column;
275             final String value = item.getAsString(column);
276             if (!TextUtils.isEmpty(value)) return false;
277         }
278         return true;
279     }
280 
281     /**
282      * Updates the editors being displayed to the user removing extra empty
283      * {@link Editor}s, so there is only max 1 empty {@link Editor} view at a time.
284      */
updateEmptyEditors(boolean shouldAnimate)285     public void updateEmptyEditors(boolean shouldAnimate) {
286 
287         final List<View> emptyEditors = getEmptyEditors();
288 
289         // If there is more than 1 empty editor, then remove it from the list of editors.
290         if (emptyEditors.size() > 1) {
291             for (final View emptyEditorView : emptyEditors) {
292                 // If no child {@link View}s are being focused on within this {@link View}, then
293                 // remove this empty editor. We can assume that at least one empty editor has focus.
294                 // The only way to get two empty editors is by deleting characters from a non-empty
295                 // editor, in which case this editor has focus.
296                 if (emptyEditorView.findFocus() == null) {
297                     final Editor editor = (Editor) emptyEditorView;
298                     if (shouldAnimate) {
299                         editor.deleteEditor();
300                     } else {
301                         mEditors.removeView(emptyEditorView);
302                     }
303                 }
304             }
305         } else if (mKind == null) {
306             // There is nothing we can do.
307             return;
308         } else if (isReadOnly()) {
309             // We don't show empty editors for read only data kinds.
310             return;
311         } else if (mKind.typeOverallMax == getEditorCount() && mKind.typeOverallMax != 0) {
312             // We have already reached the maximum number of editors. Lets not add any more.
313             return;
314         } else if (emptyEditors.size() == 1) {
315             // We have already reached the maximum number of empty editors. Lets not add any more.
316             return;
317         } else if (mShowOneEmptyEditor) {
318             final ValuesDelta values = RawContactModifier.insertChild(mState, mKind);
319             final View newField = createEditorView(values);
320             if (shouldAnimate) {
321                 newField.setVisibility(View.GONE);
322                 EditorAnimator.getInstance().showFieldFooter(newField);
323             }
324         }
325     }
326 
327     /**
328      * Returns a list of empty editor views in this section.
329      */
getEmptyEditors()330     private List<View> getEmptyEditors() {
331         List<View> emptyEditorViews = new ArrayList<View>();
332         for (int i = 0; i < mEditors.getChildCount(); i++) {
333             View view = mEditors.getChildAt(i);
334             if (((Editor) view).isEmpty()) {
335                 emptyEditorViews.add(view);
336             }
337         }
338         return emptyEditorViews;
339     }
340 
areAllEditorsEmpty()341     public boolean areAllEditorsEmpty() {
342         for (int i = 0; i < mEditors.getChildCount(); i++) {
343             final View view = mEditors.getChildAt(i);
344             if (!((Editor) view).isEmpty()) {
345                 return false;
346             }
347         }
348         return true;
349     }
350 
getEditorCount()351     public int getEditorCount() {
352         return mEditors.getChildCount();
353     }
354 
getKind()355     public DataKind getKind() {
356         return mKind;
357     }
358 
359     /**
360      * Return an icon that represents {@param mimeType}.
361      */
getMimeTypeDrawable(String mimeType)362     private Drawable getMimeTypeDrawable(String mimeType) {
363         switch (mimeType) {
364             case StructuredPostal.CONTENT_ITEM_TYPE:
365                 return getResources().getDrawable(R.drawable.ic_place_24dp);
366             case SipAddress.CONTENT_ITEM_TYPE:
367                 return getResources().getDrawable(R.drawable.ic_dialer_sip_black_24dp);
368             case Phone.CONTENT_ITEM_TYPE:
369                 return getResources().getDrawable(R.drawable.ic_phone_24dp);
370             case Im.CONTENT_ITEM_TYPE:
371                 return getResources().getDrawable(R.drawable.ic_message_24dp);
372             case Event.CONTENT_ITEM_TYPE:
373                 return getResources().getDrawable(R.drawable.ic_event_24dp);
374             case Email.CONTENT_ITEM_TYPE:
375                 return getResources().getDrawable(R.drawable.ic_email_24dp);
376             case Website.CONTENT_ITEM_TYPE:
377                 return getResources().getDrawable(R.drawable.ic_public_black_24dp);
378             case Photo.CONTENT_ITEM_TYPE:
379                 return getResources().getDrawable(R.drawable.ic_camera_alt_black_24dp);
380             case GroupMembership.CONTENT_ITEM_TYPE:
381                 return getResources().getDrawable(R.drawable.ic_people_black_24dp);
382             case Organization.CONTENT_ITEM_TYPE:
383                 return getResources().getDrawable(R.drawable.ic_business_black_24dp);
384             case Note.CONTENT_ITEM_TYPE:
385                 return getResources().getDrawable(R.drawable.ic_insert_comment_black_24dp);
386             case Relation.CONTENT_ITEM_TYPE:
387                 return getResources().getDrawable(R.drawable.ic_circles_extended_black_24dp);
388             default:
389                 return null;
390         }
391     }
392 }
393