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.provider.ContactsContract.Data;
21 import android.text.TextUtils;
22 import android.util.AttributeSet;
23 import android.view.LayoutInflater;
24 import android.view.View;
25 import android.view.ViewGroup;
26 import android.widget.ImageView;
27 import android.widget.LinearLayout;
28 
29 import com.android.contacts.R;
30 import com.android.contacts.common.model.RawContactDelta;
31 import com.android.contacts.common.model.RawContactModifier;
32 import com.android.contacts.common.model.ValuesDelta;
33 import com.android.contacts.common.model.dataitem.DataKind;
34 import com.android.contacts.editor.Editor.EditorListener;
35 
36 import java.util.ArrayList;
37 import java.util.List;
38 
39 /**
40  * Custom view for an entire section of data as segmented by
41  * {@link DataKind} around a {@link Data#MIMETYPE}. This view shows a
42  * section header and a trigger for adding new {@link Data} rows.
43  */
44 public class KindSectionView extends LinearLayout implements EditorListener {
45 
46     public interface Listener {
47 
48         /**
49          * Invoked when any editor that is displayed in this section view is deleted by the user.
50          */
onDeleteRequested(Editor editor)51         public void onDeleteRequested(Editor editor);
52     }
53 
54     private ViewGroup mEditors;
55     private ImageView mIcon;
56 
57     private DataKind mKind;
58     private RawContactDelta mState;
59     private boolean mReadOnly;
60 
61     private ViewIdGenerator mViewIdGenerator;
62 
63     private LayoutInflater mInflater;
64 
65     private Listener mListener;
66 
KindSectionView(Context context)67     public KindSectionView(Context context) {
68         this(context, null);
69     }
70 
KindSectionView(Context context, AttributeSet attrs)71     public KindSectionView(Context context, AttributeSet attrs) {
72         super(context, attrs);
73     }
74 
75     @Override
setEnabled(boolean enabled)76     public void setEnabled(boolean enabled) {
77         super.setEnabled(enabled);
78         if (mEditors != null) {
79             int childCount = mEditors.getChildCount();
80             for (int i = 0; i < childCount; i++) {
81                 mEditors.getChildAt(i).setEnabled(enabled);
82             }
83         }
84 
85         updateEmptyEditors(/* shouldAnimate = */ true);
86     }
87 
isReadOnly()88     public boolean isReadOnly() {
89         return mReadOnly;
90     }
91 
92     /** {@inheritDoc} */
93     @Override
onFinishInflate()94     protected void onFinishInflate() {
95         setDrawingCacheEnabled(true);
96         setAlwaysDrawnWithCacheEnabled(true);
97 
98         mInflater = (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
99 
100         mEditors = (ViewGroup) findViewById(R.id.kind_editors);
101         mIcon = (ImageView) findViewById(R.id.kind_icon);
102     }
103 
104     @Override
onDeleteRequested(Editor editor)105     public void onDeleteRequested(Editor editor) {
106         if (getEditorCount() == 1) {
107             // If there is only 1 editor in the section, then don't allow the user to delete it.
108             // Just clear the fields in the editor.
109             editor.clearAllFields();
110         } else {
111             // If there is a listener, let it decide whether to delete the Editor or the entire
112             // KindSectionView so that there is no jank from both animations happening in succession.
113             if (mListener != null) {
114                 editor.markDeleted();
115                 mListener.onDeleteRequested(editor);
116             } else {
117                 editor.deleteEditor();
118             }
119         }
120     }
121 
122     @Override
onRequest(int request)123     public void onRequest(int request) {
124         // If a field has become empty or non-empty, then check if another row
125         // can be added dynamically.
126         if (request == FIELD_TURNED_EMPTY || request == FIELD_TURNED_NON_EMPTY) {
127             updateEmptyEditors(/* shouldAnimate = */ true);
128         }
129     }
130 
setListener(Listener listener)131     public void setListener(Listener listener) {
132         mListener = listener;
133     }
134 
setState(DataKind kind, RawContactDelta state, boolean readOnly, ViewIdGenerator vig)135     public void setState(DataKind kind, RawContactDelta state, boolean readOnly,
136             ViewIdGenerator vig) {
137         mKind = kind;
138         mState = state;
139         mReadOnly = readOnly;
140         mViewIdGenerator = vig;
141 
142         setId(mViewIdGenerator.getId(state, kind, null, ViewIdGenerator.NO_VIEW_INDEX));
143 
144         // TODO: handle resources from remote packages
145         final String titleString = (kind.titleRes == -1 || kind.titleRes == 0)
146                 ? ""
147                 : getResources().getString(kind.titleRes);
148         mIcon.setContentDescription(titleString);
149 
150         mIcon.setImageDrawable(EditorUiUtils.getMimeTypeDrawable(getContext(), kind.mimeType));
151         if (mIcon.getDrawable() == null) {
152             mIcon.setContentDescription(null);
153         }
154 
155         rebuildFromState();
156         updateEmptyEditors(/* shouldAnimate = */ false);
157     }
158 
159     /**
160      * Build editors for all current {@link #mState} rows.
161      */
rebuildFromState()162     private void rebuildFromState() {
163         // Remove any existing editors
164         mEditors.removeAllViews();
165 
166         // Check if we are displaying anything here
167         boolean hasEntries = mState.hasMimeEntries(mKind.mimeType);
168 
169         if (hasEntries) {
170             for (ValuesDelta entry : mState.getMimeEntries(mKind.mimeType)) {
171                 // Skip entries that aren't visible
172                 if (!entry.isVisible()) continue;
173                 if (isEmptyNoop(entry)) continue;
174 
175                 createEditorView(entry);
176             }
177         }
178     }
179 
180 
181     /**
182      * Creates an EditorView for the given entry. This function must be used while constructing
183      * the views corresponding to the the object-model. The resulting EditorView is also added
184      * to the end of mEditors
185      */
createEditorView(ValuesDelta entry)186     private View createEditorView(ValuesDelta entry) {
187         final View view;
188         final int layoutResId = EditorUiUtils.getLayoutResourceId(mKind.mimeType);
189         try {
190             view = mInflater.inflate(layoutResId, mEditors, false);
191         } catch (Exception e) {
192             throw new RuntimeException(
193                     "Cannot allocate editor with layout resource ID " +
194                     layoutResId + " for MIME type " + mKind.mimeType +
195                     " with error " + e.toString());
196         }
197         view.setEnabled(isEnabled());
198         if (view instanceof Editor) {
199             Editor editor = (Editor) view;
200             editor.setDeletable(true);
201             editor.setValues(mKind, entry, mState, mReadOnly, mViewIdGenerator);
202             editor.setEditorListener(this);
203         }
204         mEditors.addView(view);
205         return view;
206     }
207 
208     /**
209      * Tests whether the given item has no changes (so it exists in the database) but is empty
210      */
isEmptyNoop(ValuesDelta item)211     private boolean isEmptyNoop(ValuesDelta item) {
212         if (!item.isNoop()) return false;
213         final int fieldCount = mKind.fieldList.size();
214         for (int i = 0; i < fieldCount; i++) {
215             final String column = mKind.fieldList.get(i).column;
216             final String value = item.getAsString(column);
217             if (!TextUtils.isEmpty(value)) return false;
218         }
219         return true;
220     }
221 
222     /**
223      * Updates the editors being displayed to the user removing extra empty
224      * {@link Editor}s, so there is only max 1 empty {@link Editor} view at a time.
225      */
updateEmptyEditors(boolean shouldAnimate)226     public void updateEmptyEditors(boolean shouldAnimate) {
227 
228         final List<View> emptyEditors = getEmptyEditors();
229 
230         // If there is more than 1 empty editor, then remove it from the list of editors.
231         if (emptyEditors.size() > 1) {
232             for (final View emptyEditorView : emptyEditors) {
233                 // If no child {@link View}s are being focused on within this {@link View}, then
234                 // remove this empty editor. We can assume that at least one empty editor has focus.
235                 // The only way to get two empty editors is by deleting characters from a non-empty
236                 // editor, in which case this editor has focus.
237                 if (emptyEditorView.findFocus() == null) {
238                     final Editor editor = (Editor) emptyEditorView;
239                     if (shouldAnimate) {
240                         editor.deleteEditor();
241                     } else {
242                         mEditors.removeView(emptyEditorView);
243                     }
244                 }
245             }
246         } else if (mKind == null) {
247             // There is nothing we can do.
248             return;
249         } else if (isReadOnly()) {
250             // We don't show empty editors for read only data kinds.
251             return;
252         } else if (!RawContactModifier.canInsert(mState, mKind)) {
253             // We have already reached the maximum number of editors. Lets not add any more.
254             return;
255         } else if (emptyEditors.size() == 1) {
256             // We have already reached the maximum number of empty editors. Lets not add any more.
257             return;
258         } else {
259             final ValuesDelta values = RawContactModifier.insertChild(mState, mKind);
260             final View newField = createEditorView(values);
261             if (shouldAnimate) {
262                 newField.setVisibility(View.GONE);
263                 EditorAnimator.getInstance().showFieldFooter(newField);
264             }
265         }
266     }
267 
268     /**
269      * Returns a list of empty editor views in this section.
270      */
getEmptyEditors()271     private List<View> getEmptyEditors() {
272         List<View> emptyEditorViews = new ArrayList<View>();
273         for (int i = 0; i < mEditors.getChildCount(); i++) {
274             View view = mEditors.getChildAt(i);
275             if (((Editor) view).isEmpty()) {
276                 emptyEditorViews.add(view);
277             }
278         }
279         return emptyEditorViews;
280     }
281 
getEditorCount()282     public int getEditorCount() {
283         return mEditors.getChildCount();
284     }
285 
getKind()286     public DataKind getKind() {
287         return mKind;
288     }
289 }
290