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