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.editor.Editor.EditorListener;
44 import com.android.contacts.common.model.RawContactModifier;
45 import com.android.contacts.common.model.RawContactDelta;
46 import com.android.contacts.common.model.ValuesDelta;
47 import com.android.contacts.common.model.dataitem.DataKind;
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     private static final String TAG = "KindSectionView";
59 
60     private ViewGroup mEditors;
61     private ImageView mIcon;
62 
63     private DataKind mKind;
64     private RawContactDelta mState;
65     private boolean mReadOnly;
66 
67     private ViewIdGenerator mViewIdGenerator;
68 
69     private LayoutInflater mInflater;
70 
KindSectionView(Context context)71     public KindSectionView(Context context) {
72         this(context, null);
73     }
74 
KindSectionView(Context context, AttributeSet attrs)75     public KindSectionView(Context context, AttributeSet attrs) {
76         super(context, attrs);
77     }
78 
79     @Override
setEnabled(boolean enabled)80     public void setEnabled(boolean enabled) {
81         super.setEnabled(enabled);
82         if (mEditors != null) {
83             int childCount = mEditors.getChildCount();
84             for (int i = 0; i < childCount; i++) {
85                 mEditors.getChildAt(i).setEnabled(enabled);
86             }
87         }
88 
89         updateEmptyEditors(/* shouldAnimate = */ true);
90     }
91 
isReadOnly()92     public boolean isReadOnly() {
93         return mReadOnly;
94     }
95 
96     /** {@inheritDoc} */
97     @Override
onFinishInflate()98     protected void onFinishInflate() {
99         setDrawingCacheEnabled(true);
100         setAlwaysDrawnWithCacheEnabled(true);
101 
102         mInflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
103 
104         mEditors = (ViewGroup) findViewById(R.id.kind_editors);
105         mIcon = (ImageView) findViewById(R.id.kind_icon);
106     }
107 
108     @Override
onDeleteRequested(Editor editor)109     public void onDeleteRequested(Editor editor) {
110         // If there is only 1 editor in the section, then don't allow the user to delete it.
111         // Just clear the fields in the editor.
112         if (getEditorCount() == 1) {
113             editor.clearAllFields();
114         } else {
115             // Otherwise it's okay to delete this {@link Editor}
116             editor.deleteEditor();
117         }
118     }
119 
120     @Override
onRequest(int request)121     public void onRequest(int request) {
122         // If a field has become empty or non-empty, then check if another row
123         // can be added dynamically.
124         if (request == FIELD_TURNED_EMPTY || request == FIELD_TURNED_NON_EMPTY) {
125             updateEmptyEditors(/* shouldAnimate = */ true);
126         }
127     }
128 
setState(DataKind kind, RawContactDelta state, boolean readOnly, ViewIdGenerator vig)129     public void setState(DataKind kind, RawContactDelta state, boolean readOnly, ViewIdGenerator vig) {
130         mKind = kind;
131         mState = state;
132         mReadOnly = readOnly;
133         mViewIdGenerator = vig;
134 
135         setId(mViewIdGenerator.getId(state, kind, null, ViewIdGenerator.NO_VIEW_INDEX));
136 
137         // TODO: handle resources from remote packages
138         final String titleString = (kind.titleRes == -1 || kind.titleRes == 0)
139                 ? ""
140                 : getResources().getString(kind.titleRes);
141         mIcon.setContentDescription(titleString);
142 
143         mIcon.setImageDrawable(getMimeTypeDrawable(kind.mimeType));
144         if (mIcon.getDrawable() == null) {
145             mIcon.setContentDescription(null);
146         }
147 
148         rebuildFromState();
149         updateEmptyEditors(/* shouldAnimate = */ false);
150     }
151 
152     /**
153      * Build editors for all current {@link #mState} rows.
154      */
rebuildFromState()155     private void rebuildFromState() {
156         // Remove any existing editors
157         mEditors.removeAllViews();
158 
159         // Check if we are displaying anything here
160         boolean hasEntries = mState.hasMimeEntries(mKind.mimeType);
161 
162         if (hasEntries) {
163             for (ValuesDelta entry : mState.getMimeEntries(mKind.mimeType)) {
164                 // Skip entries that aren't visible
165                 if (!entry.isVisible()) continue;
166                 if (isEmptyNoop(entry)) continue;
167 
168                 createEditorView(entry);
169             }
170         }
171     }
172 
173 
174     /**
175      * Creates an EditorView for the given entry. This function must be used while constructing
176      * the views corresponding to the the object-model. The resulting EditorView is also added
177      * to the end of mEditors
178      */
createEditorView(ValuesDelta entry)179     private View createEditorView(ValuesDelta entry) {
180         final View view;
181         final int layoutResId = EditorUiUtils.getLayoutResourceId(mKind.mimeType);
182         try {
183             view = mInflater.inflate(layoutResId, mEditors, false);
184         } catch (Exception e) {
185             throw new RuntimeException(
186                     "Cannot allocate editor with layout resource ID " +
187                     layoutResId + " for MIME type " + mKind.mimeType +
188                     " with error " + e.toString());
189         }
190 
191         view.setEnabled(isEnabled());
192 
193         if (view instanceof Editor) {
194             Editor editor = (Editor) view;
195             editor.setDeletable(true);
196             editor.setValues(mKind, entry, mState, mReadOnly, mViewIdGenerator);
197             editor.setEditorListener(this);
198         }
199         mEditors.addView(view);
200         return view;
201     }
202 
203     /**
204      * Tests whether the given item has no changes (so it exists in the database) but is empty
205      */
isEmptyNoop(ValuesDelta item)206     private boolean isEmptyNoop(ValuesDelta item) {
207         if (!item.isNoop()) return false;
208         final int fieldCount = mKind.fieldList.size();
209         for (int i = 0; i < fieldCount; i++) {
210             final String column = mKind.fieldList.get(i).column;
211             final String value = item.getAsString(column);
212             if (!TextUtils.isEmpty(value)) return false;
213         }
214         return true;
215     }
216 
217     /**
218      * Updates the editors being displayed to the user removing extra empty
219      * {@link Editor}s, so there is only max 1 empty {@link Editor} view at a time.
220      */
updateEmptyEditors(boolean shouldAnimate)221     private void updateEmptyEditors(boolean shouldAnimate) {
222 
223         final List<View> emptyEditors = getEmptyEditors();
224 
225         // If there is more than 1 empty editor, then remove it from the list of editors.
226         if (emptyEditors.size() > 1) {
227             for (final View emptyEditorView : emptyEditors) {
228                 // If no child {@link View}s are being focused on within this {@link View}, then
229                 // remove this empty editor. We can assume that at least one empty editor has focus.
230                 // The only way to get two empty editors is by deleting characters from a non-empty
231                 // editor, in which case this editor has focus.
232                 if (emptyEditorView.findFocus() == null) {
233                     final Editor editor = (Editor) emptyEditorView;
234                     if (shouldAnimate) {
235                         editor.deleteEditor();
236                     } else {
237                         mEditors.removeView(emptyEditorView);
238                     }
239                 }
240             }
241         } else if (mKind == null) {
242             // There is nothing we can do.
243             return;
244         } else if (isReadOnly()) {
245             // We don't show empty editors for read only data kinds.
246             return;
247         } else if (mKind.typeOverallMax == getEditorCount() && mKind.typeOverallMax != 0) {
248             // We have already reached the maximum number of editors. Lets not add any more.
249             return;
250         } else if (emptyEditors.size() == 1) {
251             // We have already reached the maximum number of empty editors. Lets not add any more.
252             return;
253         } else {
254             final ValuesDelta values = RawContactModifier.insertChild(mState, mKind);
255             final View newField = createEditorView(values);
256             if (shouldAnimate) {
257                 newField.setVisibility(View.GONE);
258                 EditorAnimator.getInstance().showFieldFooter(newField);
259             }
260         }
261     }
262 
263     /**
264      * Returns a list of empty editor views in this section.
265      */
getEmptyEditors()266     private List<View> getEmptyEditors() {
267         List<View> emptyEditorViews = new ArrayList<View>();
268         for (int i = 0; i < mEditors.getChildCount(); i++) {
269             View view = mEditors.getChildAt(i);
270             if (((Editor) view).isEmpty()) {
271                 emptyEditorViews.add(view);
272             }
273         }
274         return emptyEditorViews;
275     }
276 
getEditorCount()277     public int getEditorCount() {
278         return mEditors.getChildCount();
279     }
280 
getKind()281     public DataKind getKind() {
282         return mKind;
283     }
284 
285     /**
286      * Return an icon that represents {@param mimeType}.
287      */
getMimeTypeDrawable(String mimeType)288     private Drawable getMimeTypeDrawable(String mimeType) {
289         switch (mimeType) {
290             case StructuredPostal.CONTENT_ITEM_TYPE:
291                 return getResources().getDrawable(R.drawable.ic_place_24dp);
292             case SipAddress.CONTENT_ITEM_TYPE:
293                 return getResources().getDrawable(R.drawable.ic_dialer_sip_black_24dp);
294             case Phone.CONTENT_ITEM_TYPE:
295                 return getResources().getDrawable(R.drawable.ic_phone_24dp);
296             case Im.CONTENT_ITEM_TYPE:
297                 return getResources().getDrawable(R.drawable.ic_message_24dp);
298             case Event.CONTENT_ITEM_TYPE:
299                 return getResources().getDrawable(R.drawable.ic_event_24dp);
300             case Email.CONTENT_ITEM_TYPE:
301                 return getResources().getDrawable(R.drawable.ic_email_24dp);
302             case Website.CONTENT_ITEM_TYPE:
303                 return getResources().getDrawable(R.drawable.ic_public_black_24dp);
304             case Photo.CONTENT_ITEM_TYPE:
305                 return getResources().getDrawable(R.drawable.ic_camera_alt_black_24dp);
306             case GroupMembership.CONTENT_ITEM_TYPE:
307                 return getResources().getDrawable(R.drawable.ic_people_black_24dp);
308             case Organization.CONTENT_ITEM_TYPE:
309                 return getResources().getDrawable(R.drawable.ic_business_black_24dp);
310             case Note.CONTENT_ITEM_TYPE:
311                 return getResources().getDrawable(R.drawable.ic_insert_comment_black_24dp);
312             case Relation.CONTENT_ITEM_TYPE:
313                 return getResources().getDrawable(R.drawable.ic_circles_extended_black_24dp);
314             default:
315                 return null;
316         }
317     }
318 }
319