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 com.android.contacts.R;
20import com.android.contacts.editor.Editor.EditorListener;
21import com.android.contacts.model.DataKind;
22import com.android.contacts.model.EntityDelta;
23import com.android.contacts.model.EntityDelta.ValuesDelta;
24import com.android.contacts.model.EntityModifier;
25
26import android.content.Context;
27import android.text.TextUtils;
28import android.util.AttributeSet;
29import android.view.LayoutInflater;
30import android.view.View;
31import android.view.ViewGroup;
32import android.widget.LinearLayout;
33import android.widget.TextView;
34
35import java.util.ArrayList;
36import java.util.List;
37
38/**
39 * Custom view for an entire section of data as segmented by
40 * {@link DataKind} around a {@link Data#MIMETYPE}. This view shows a
41 * section header and a trigger for adding new {@link Data} rows.
42 */
43public class KindSectionView extends LinearLayout implements EditorListener {
44    private static final String TAG = "KindSectionView";
45
46    private TextView mTitle;
47    private ViewGroup mEditors;
48    private View mAddFieldFooter;
49    private String mTitleString;
50
51    private DataKind mKind;
52    private EntityDelta mState;
53    private boolean mReadOnly;
54
55    private ViewIdGenerator mViewIdGenerator;
56
57    private LayoutInflater mInflater;
58
59    public KindSectionView(Context context) {
60        this(context, null);
61    }
62
63    public KindSectionView(Context context, AttributeSet attrs) {
64        super(context, attrs);
65    }
66
67    @Override
68    public void setEnabled(boolean enabled) {
69        super.setEnabled(enabled);
70        if (mEditors != null) {
71            int childCount = mEditors.getChildCount();
72            for (int i = 0; i < childCount; i++) {
73                mEditors.getChildAt(i).setEnabled(enabled);
74            }
75        }
76
77        if (enabled && !mReadOnly) {
78            mAddFieldFooter.setVisibility(View.VISIBLE);
79        } else {
80            mAddFieldFooter.setVisibility(View.GONE);
81        }
82    }
83
84    public boolean isReadOnly() {
85        return mReadOnly;
86    }
87
88    /** {@inheritDoc} */
89    @Override
90    protected void onFinishInflate() {
91        setDrawingCacheEnabled(true);
92        setAlwaysDrawnWithCacheEnabled(true);
93
94        mInflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
95
96        mTitle = (TextView) findViewById(R.id.kind_title);
97        mEditors = (ViewGroup) findViewById(R.id.kind_editors);
98        mAddFieldFooter = findViewById(R.id.add_field_footer);
99        mAddFieldFooter.setOnClickListener(new OnClickListener() {
100            @Override
101            public void onClick(View v) {
102                // Setup click listener to add an empty field when the footer is clicked.
103                mAddFieldFooter.setVisibility(View.GONE);
104                addItem();
105            }
106        });
107    }
108
109    @Override
110    public void onDeleteRequested(Editor editor) {
111        // If there is only 1 editor in the section, then don't allow the user to delete it.
112        // Just clear the fields in the editor.
113        if (getEditorCount() == 1) {
114            editor.clearAllFields();
115        } else {
116            // Otherwise it's okay to delete this {@link Editor}
117            editor.deleteEditor();
118        }
119        updateAddFooterVisible();
120    }
121
122    @Override
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            updateAddFooterVisible();
128        }
129    }
130
131    public void setState(DataKind kind, EntityDelta state, boolean readOnly, ViewIdGenerator vig) {
132        mKind = kind;
133        mState = state;
134        mReadOnly = readOnly;
135        mViewIdGenerator = vig;
136
137        setId(mViewIdGenerator.getId(state, kind, null, ViewIdGenerator.NO_VIEW_INDEX));
138
139        // TODO: handle resources from remote packages
140        mTitleString = (kind.titleRes == -1 || kind.titleRes == 0)
141                ? ""
142                : getResources().getString(kind.titleRes);
143        mTitle.setText(mTitleString);
144
145        rebuildFromState();
146        updateAddFooterVisible();
147        updateSectionVisible();
148    }
149
150    public String getTitle() {
151        return mTitleString;
152    }
153
154    public void setTitleVisible(boolean visible) {
155        findViewById(R.id.kind_title_layout).setVisibility(visible ? View.VISIBLE : View.GONE);
156    }
157
158    /**
159     * Build editors for all current {@link #mState} rows.
160     */
161    public void rebuildFromState() {
162        // Remove any existing editors
163        mEditors.removeAllViews();
164
165        // Check if we are displaying anything here
166        boolean hasEntries = mState.hasMimeEntries(mKind.mimeType);
167
168        if (hasEntries) {
169            for (ValuesDelta entry : mState.getMimeEntries(mKind.mimeType)) {
170                // Skip entries that aren't visible
171                if (!entry.isVisible()) continue;
172                if (isEmptyNoop(entry)) continue;
173
174                createEditorView(entry);
175            }
176        }
177    }
178
179
180    /**
181     * Creates an EditorView for the given entry. This function must be used while constructing
182     * the views corresponding to the the object-model. The resulting EditorView is also added
183     * to the end of mEditors
184     */
185    private View createEditorView(ValuesDelta entry) {
186        final View view;
187        try {
188            view = mInflater.inflate(mKind.editorLayoutResourceId, mEditors, false);
189        } catch (Exception e) {
190            throw new RuntimeException(
191                    "Cannot allocate editor with layout resource ID " +
192                    mKind.editorLayoutResourceId + " for MIME type " + mKind.mimeType +
193                    " with error " + e.toString());
194        }
195
196        view.setEnabled(isEnabled());
197
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     */
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    private void updateSectionVisible() {
223        setVisibility(getEditorCount() != 0 ? VISIBLE : GONE);
224    }
225
226    protected void updateAddFooterVisible() {
227        if (!mReadOnly && (mKind.typeOverallMax != 1)) {
228            // First determine whether there are any existing empty editors.
229            updateEmptyEditors();
230            // If there are no existing empty editors and it's possible to add
231            // another field, then make the "add footer" field visible.
232            if (!hasEmptyEditor() && EntityModifier.canInsert(mState, mKind)) {
233                mAddFieldFooter.setVisibility(View.VISIBLE);
234                return;
235            }
236        }
237        mAddFieldFooter.setVisibility(View.GONE);
238    }
239
240    /**
241     * Updates the editors being displayed to the user removing extra empty
242     * {@link Editor}s, so there is only max 1 empty {@link Editor} view at a time.
243     */
244    private void updateEmptyEditors() {
245        List<View> emptyEditors = getEmptyEditors();
246
247        // If there is more than 1 empty editor, then remove it from the list of editors.
248        if (emptyEditors.size() > 1) {
249            for (View emptyEditorView : emptyEditors) {
250                // If no child {@link View}s are being focused on within
251                // this {@link View}, then remove this empty editor.
252                if (emptyEditorView.findFocus() == null) {
253                    mEditors.removeView(emptyEditorView);
254                }
255            }
256        }
257    }
258
259    /**
260     * Returns a list of empty editor views in this section.
261     */
262    private List<View> getEmptyEditors() {
263        List<View> emptyEditorViews = new ArrayList<View>();
264        for (int i = 0; i < mEditors.getChildCount(); i++) {
265            View view = mEditors.getChildAt(i);
266            if (((Editor) view).isEmpty()) {
267                emptyEditorViews.add(view);
268            }
269        }
270        return emptyEditorViews;
271    }
272
273    /**
274     * Returns true if one of the editors has all of its fields empty, or false
275     * otherwise.
276     */
277    private boolean hasEmptyEditor() {
278        return getEmptyEditors().size() > 0;
279    }
280
281    /**
282     * Returns true if all editors are empty.
283     */
284    public boolean isEmpty() {
285        for (int i = 0; i < mEditors.getChildCount(); i++) {
286            View view = mEditors.getChildAt(i);
287            if (!((Editor) view).isEmpty()) {
288                return false;
289            }
290        }
291        return true;
292    }
293
294    public void addItem() {
295        ValuesDelta values = null;
296        // If this is a list, we can freely add. If not, only allow adding the first.
297        if (mKind.typeOverallMax == 1) {
298            if (getEditorCount() == 1) {
299                return;
300            }
301
302            // If we already have an item, just make it visible
303            ArrayList<ValuesDelta> entries = mState.getMimeEntries(mKind.mimeType);
304            if (entries != null && entries.size() > 0) {
305                values = entries.get(0);
306            }
307        }
308
309        // Insert a new child, create its view and set its focus
310        if (values == null) {
311            values = EntityModifier.insertChild(mState, mKind);
312        }
313
314        final View newField = createEditorView(values);
315        post(new Runnable() {
316
317            @Override
318            public void run() {
319                newField.requestFocus();
320            }
321        });
322
323        // Hide the "add field" footer because there is now a blank field.
324        mAddFieldFooter.setVisibility(View.GONE);
325
326        // Ensure we are visible
327        updateSectionVisible();
328    }
329
330    public int getEditorCount() {
331        return mEditors.getChildCount();
332    }
333
334    public DataKind getKind() {
335        return mKind;
336    }
337}
338