• 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 com.android.contacts.R;
20 import com.android.contacts.editor.Editor.EditorListener;
21 import com.android.contacts.model.DataKind;
22 import com.android.contacts.model.EntityDelta;
23 import com.android.contacts.model.EntityDelta.ValuesDelta;
24 import com.android.contacts.model.EntityModifier;
25 
26 import android.content.Context;
27 import android.provider.ContactsContract.Data;
28 import android.text.TextUtils;
29 import android.util.AttributeSet;
30 import android.view.LayoutInflater;
31 import android.view.View;
32 import android.view.ViewGroup;
33 import android.widget.LinearLayout;
34 import android.widget.TextView;
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     private static final String TAG = "KindSectionView";
46 
47     private TextView mTitle;
48     private ViewGroup mEditors;
49     private View mAddFieldFooter;
50     private String mTitleString;
51 
52     private DataKind mKind;
53     private EntityDelta mState;
54     private boolean mReadOnly;
55 
56     private ViewIdGenerator mViewIdGenerator;
57 
58     private LayoutInflater mInflater;
59 
60     private final ArrayList<Runnable> mRunWhenWindowFocused = new ArrayList<Runnable>(1);
61 
KindSectionView(Context context)62     public KindSectionView(Context context) {
63         this(context, null);
64     }
65 
KindSectionView(Context context, AttributeSet attrs)66     public KindSectionView(Context context, AttributeSet attrs) {
67         super(context, attrs);
68     }
69 
70     @Override
setEnabled(boolean enabled)71     public void setEnabled(boolean enabled) {
72         super.setEnabled(enabled);
73         if (mEditors != null) {
74             int childCount = mEditors.getChildCount();
75             for (int i = 0; i < childCount; i++) {
76                 mEditors.getChildAt(i).setEnabled(enabled);
77             }
78         }
79 
80         if (enabled && !mReadOnly) {
81             mAddFieldFooter.setVisibility(View.VISIBLE);
82         } else {
83             mAddFieldFooter.setVisibility(View.GONE);
84         }
85     }
86 
isReadOnly()87     public boolean isReadOnly() {
88         return mReadOnly;
89     }
90 
91     /** {@inheritDoc} */
92     @Override
onFinishInflate()93     protected void onFinishInflate() {
94         setDrawingCacheEnabled(true);
95         setAlwaysDrawnWithCacheEnabled(true);
96 
97         mInflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
98 
99         mTitle = (TextView) findViewById(R.id.kind_title);
100         mEditors = (ViewGroup) findViewById(R.id.kind_editors);
101         mAddFieldFooter = findViewById(R.id.add_field_footer);
102         mAddFieldFooter.setOnClickListener(new OnClickListener() {
103             @Override
104             public void onClick(View v) {
105                 // Setup click listener to add an empty field when the footer is clicked.
106                 mAddFieldFooter.setVisibility(View.GONE);
107                 addItem();
108             }
109         });
110     }
111 
112     @Override
onDeleteRequested(Editor editor)113     public void onDeleteRequested(Editor editor) {
114         // If there is only 1 editor in the section, then don't allow the user to delete it.
115         // Just clear the fields in the editor.
116         final boolean animate;
117         if (getEditorCount() == 1) {
118             editor.clearAllFields();
119             animate = true;
120         } else {
121             // Otherwise it's okay to delete this {@link Editor}
122             editor.deleteEditor();
123 
124             // This is already animated, don't do anything further here
125             animate = false;
126         }
127         updateAddFooterVisible(animate);
128     }
129 
130     @Override
onRequest(int request)131     public void onRequest(int request) {
132         // If a field has become empty or non-empty, then check if another row
133         // can be added dynamically.
134         if (request == FIELD_TURNED_EMPTY || request == FIELD_TURNED_NON_EMPTY) {
135             updateAddFooterVisible(true);
136         }
137     }
138 
setState(DataKind kind, EntityDelta state, boolean readOnly, ViewIdGenerator vig)139     public void setState(DataKind kind, EntityDelta state, boolean readOnly, ViewIdGenerator vig) {
140         mKind = kind;
141         mState = state;
142         mReadOnly = readOnly;
143         mViewIdGenerator = vig;
144 
145         setId(mViewIdGenerator.getId(state, kind, null, ViewIdGenerator.NO_VIEW_INDEX));
146 
147         // TODO: handle resources from remote packages
148         mTitleString = (kind.titleRes == -1 || kind.titleRes == 0)
149                 ? ""
150                 : getResources().getString(kind.titleRes);
151         mTitle.setText(mTitleString);
152 
153         rebuildFromState();
154         updateAddFooterVisible(false);
155         updateSectionVisible();
156     }
157 
getTitle()158     public String getTitle() {
159         return mTitleString;
160     }
161 
setTitleVisible(boolean visible)162     public void setTitleVisible(boolean visible) {
163         findViewById(R.id.kind_title_layout).setVisibility(visible ? View.VISIBLE : View.GONE);
164     }
165 
166     /**
167      * Build editors for all current {@link #mState} rows.
168      */
rebuildFromState()169     public void rebuildFromState() {
170         // Remove any existing editors
171         mEditors.removeAllViews();
172 
173         // Check if we are displaying anything here
174         boolean hasEntries = mState.hasMimeEntries(mKind.mimeType);
175 
176         if (hasEntries) {
177             for (ValuesDelta entry : mState.getMimeEntries(mKind.mimeType)) {
178                 // Skip entries that aren't visible
179                 if (!entry.isVisible()) continue;
180                 if (isEmptyNoop(entry)) continue;
181 
182                 createEditorView(entry);
183             }
184         }
185     }
186 
187 
188     /**
189      * Creates an EditorView for the given entry. This function must be used while constructing
190      * the views corresponding to the the object-model. The resulting EditorView is also added
191      * to the end of mEditors
192      */
createEditorView(ValuesDelta entry)193     private View createEditorView(ValuesDelta entry) {
194         final View view;
195         try {
196             view = mInflater.inflate(mKind.editorLayoutResourceId, mEditors, false);
197         } catch (Exception e) {
198             throw new RuntimeException(
199                     "Cannot allocate editor with layout resource ID " +
200                     mKind.editorLayoutResourceId + " for MIME type " + mKind.mimeType +
201                     " with error " + e.toString());
202         }
203 
204         view.setEnabled(isEnabled());
205 
206         if (view instanceof Editor) {
207             Editor editor = (Editor) view;
208             editor.setDeletable(true);
209             editor.setValues(mKind, entry, mState, mReadOnly, mViewIdGenerator);
210             editor.setEditorListener(this);
211         }
212         mEditors.addView(view);
213         return view;
214     }
215 
216     /**
217      * Tests whether the given item has no changes (so it exists in the database) but is empty
218      */
isEmptyNoop(ValuesDelta item)219     private boolean isEmptyNoop(ValuesDelta item) {
220         if (!item.isNoop()) return false;
221         final int fieldCount = mKind.fieldList.size();
222         for (int i = 0; i < fieldCount; i++) {
223             final String column = mKind.fieldList.get(i).column;
224             final String value = item.getAsString(column);
225             if (!TextUtils.isEmpty(value)) return false;
226         }
227         return true;
228     }
229 
updateSectionVisible()230     private void updateSectionVisible() {
231         setVisibility(getEditorCount() != 0 ? VISIBLE : GONE);
232     }
233 
updateAddFooterVisible(boolean animate)234     protected void updateAddFooterVisible(boolean animate) {
235         if (!mReadOnly && (mKind.typeOverallMax != 1)) {
236             // First determine whether there are any existing empty editors.
237             updateEmptyEditors();
238             // If there are no existing empty editors and it's possible to add
239             // another field, then make the "add footer" field visible.
240             if (!hasEmptyEditor() && EntityModifier.canInsert(mState, mKind)) {
241                 if (animate) {
242                     EditorAnimator.getInstance().showAddFieldFooter(mAddFieldFooter);
243                 } else {
244                     mAddFieldFooter.setVisibility(View.VISIBLE);
245                 }
246                 return;
247             }
248         }
249         if (animate) {
250             EditorAnimator.getInstance().hideAddFieldFooter(mAddFieldFooter);
251         } else {
252             mAddFieldFooter.setVisibility(View.GONE);
253         }
254     }
255 
256     /**
257      * Updates the editors being displayed to the user removing extra empty
258      * {@link Editor}s, so there is only max 1 empty {@link Editor} view at a time.
259      */
updateEmptyEditors()260     private void updateEmptyEditors() {
261         List<View> emptyEditors = getEmptyEditors();
262 
263         // If there is more than 1 empty editor, then remove it from the list of editors.
264         if (emptyEditors.size() > 1) {
265             for (View emptyEditorView : emptyEditors) {
266                 // If no child {@link View}s are being focused on within
267                 // this {@link View}, then remove this empty editor.
268                 if (emptyEditorView.findFocus() == null) {
269                     mEditors.removeView(emptyEditorView);
270                 }
271             }
272         }
273     }
274 
275     /**
276      * Returns a list of empty editor views in this section.
277      */
getEmptyEditors()278     private List<View> getEmptyEditors() {
279         List<View> emptyEditorViews = new ArrayList<View>();
280         for (int i = 0; i < mEditors.getChildCount(); i++) {
281             View view = mEditors.getChildAt(i);
282             if (((Editor) view).isEmpty()) {
283                 emptyEditorViews.add(view);
284             }
285         }
286         return emptyEditorViews;
287     }
288 
289     /**
290      * Returns true if one of the editors has all of its fields empty, or false
291      * otherwise.
292      */
hasEmptyEditor()293     private boolean hasEmptyEditor() {
294         return getEmptyEditors().size() > 0;
295     }
296 
297     /**
298      * Returns true if all editors are empty.
299      */
isEmpty()300     public boolean isEmpty() {
301         for (int i = 0; i < mEditors.getChildCount(); i++) {
302             View view = mEditors.getChildAt(i);
303             if (!((Editor) view).isEmpty()) {
304                 return false;
305             }
306         }
307         return true;
308     }
309 
310     /**
311      * Extends superclass implementation to also run tasks
312      * enqueued by {@link #runWhenWindowFocused}.
313      */
314     @Override
onWindowFocusChanged(boolean hasWindowFocus)315     public void onWindowFocusChanged(boolean hasWindowFocus) {
316         super.onWindowFocusChanged(hasWindowFocus);
317         if (hasWindowFocus) {
318             for (Runnable r: mRunWhenWindowFocused) {
319                 r.run();
320             }
321             mRunWhenWindowFocused.clear();
322         }
323     }
324 
325     /**
326      * Depending on whether we are in the currently-focused window, either run
327      * the argument immediately, or stash it until our window becomes focused.
328      */
runWhenWindowFocused(Runnable r)329     private void runWhenWindowFocused(Runnable r) {
330         if (hasWindowFocus()) {
331             r.run();
332         } else {
333             mRunWhenWindowFocused.add(r);
334         }
335     }
336 
337     /**
338      * Simple wrapper around {@link #runWhenWindowFocused}
339      * to ensure that it runs in the UI thread.
340      */
postWhenWindowFocused(final Runnable r)341     private void postWhenWindowFocused(final Runnable r) {
342         post(new Runnable() {
343             @Override
344             public void run() {
345                 runWhenWindowFocused(r);
346             }
347         });
348     }
349 
addItem()350     public void addItem() {
351         ValuesDelta values = null;
352         // If this is a list, we can freely add. If not, only allow adding the first.
353         if (mKind.typeOverallMax == 1) {
354             if (getEditorCount() == 1) {
355                 return;
356             }
357 
358             // If we already have an item, just make it visible
359             ArrayList<ValuesDelta> entries = mState.getMimeEntries(mKind.mimeType);
360             if (entries != null && entries.size() > 0) {
361                 values = entries.get(0);
362             }
363         }
364 
365         // Insert a new child, create its view and set its focus
366         if (values == null) {
367             values = EntityModifier.insertChild(mState, mKind);
368         }
369 
370         final View newField = createEditorView(values);
371         if (newField instanceof Editor) {
372             postWhenWindowFocused(new Runnable() {
373                 @Override
374                 public void run() {
375                     newField.requestFocus();
376                     ((Editor)newField).editNewlyAddedField();
377                 }
378             });
379         }
380 
381         // Hide the "add field" footer because there is now a blank field.
382         mAddFieldFooter.setVisibility(View.GONE);
383 
384         // Ensure we are visible
385         updateSectionVisible();
386     }
387 
getEditorCount()388     public int getEditorCount() {
389         return mEditors.getChildCount();
390     }
391 
getKind()392     public DataKind getKind() {
393         return mKind;
394     }
395 }
396