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.common.model.RawContactModifier;
32import com.android.contacts.common.model.RawContactDelta;
33import com.android.contacts.common.model.ValuesDelta;
34import com.android.contacts.common.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        final int layoutResId = EditorUiUtils.getLayoutResourceId(mKind.mimeType);
190        try {
191            view = mInflater.inflate(layoutResId, mEditors, false);
192        } catch (Exception e) {
193            throw new RuntimeException(
194                    "Cannot allocate editor with layout resource ID " +
195                    layoutResId + " for MIME type " + mKind.mimeType +
196                    " with error " + e.toString());
197        }
198
199        view.setEnabled(isEnabled());
200
201        if (view instanceof Editor) {
202            Editor editor = (Editor) view;
203            editor.setDeletable(true);
204            editor.setValues(mKind, entry, mState, mReadOnly, mViewIdGenerator);
205            editor.setEditorListener(this);
206        }
207        mEditors.addView(view);
208        return view;
209    }
210
211    /**
212     * Tests whether the given item has no changes (so it exists in the database) but is empty
213     */
214    private boolean isEmptyNoop(ValuesDelta item) {
215        if (!item.isNoop()) return false;
216        final int fieldCount = mKind.fieldList.size();
217        for (int i = 0; i < fieldCount; i++) {
218            final String column = mKind.fieldList.get(i).column;
219            final String value = item.getAsString(column);
220            if (!TextUtils.isEmpty(value)) return false;
221        }
222        return true;
223    }
224
225    private void updateSectionVisible() {
226        setVisibility(getEditorCount() != 0 ? VISIBLE : GONE);
227    }
228
229    protected void updateAddFooterVisible(boolean animate) {
230        if (!mReadOnly && (mKind.typeOverallMax != 1)) {
231            // First determine whether there are any existing empty editors.
232            updateEmptyEditors();
233            // If there are no existing empty editors and it's possible to add
234            // another field, then make the "add footer" field visible.
235            if (!hasEmptyEditor() && RawContactModifier.canInsert(mState, mKind)) {
236                if (animate) {
237                    EditorAnimator.getInstance().showAddFieldFooter(mAddFieldFooter);
238                } else {
239                    mAddFieldFooter.setVisibility(View.VISIBLE);
240                }
241                return;
242            }
243        }
244        if (animate) {
245            EditorAnimator.getInstance().hideAddFieldFooter(mAddFieldFooter);
246        } else {
247            mAddFieldFooter.setVisibility(View.GONE);
248        }
249    }
250
251    /**
252     * Updates the editors being displayed to the user removing extra empty
253     * {@link Editor}s, so there is only max 1 empty {@link Editor} view at a time.
254     */
255    private void updateEmptyEditors() {
256        List<View> emptyEditors = getEmptyEditors();
257
258        // If there is more than 1 empty editor, then remove it from the list of editors.
259        if (emptyEditors.size() > 1) {
260            for (View emptyEditorView : emptyEditors) {
261                // If no child {@link View}s are being focused on within
262                // this {@link View}, then remove this empty editor.
263                if (emptyEditorView.findFocus() == null) {
264                    mEditors.removeView(emptyEditorView);
265                }
266            }
267        }
268    }
269
270    /**
271     * Returns a list of empty editor views in this section.
272     */
273    private List<View> getEmptyEditors() {
274        List<View> emptyEditorViews = new ArrayList<View>();
275        for (int i = 0; i < mEditors.getChildCount(); i++) {
276            View view = mEditors.getChildAt(i);
277            if (((Editor) view).isEmpty()) {
278                emptyEditorViews.add(view);
279            }
280        }
281        return emptyEditorViews;
282    }
283
284    /**
285     * Returns true if one of the editors has all of its fields empty, or false
286     * otherwise.
287     */
288    private boolean hasEmptyEditor() {
289        return getEmptyEditors().size() > 0;
290    }
291
292    /**
293     * Returns true if all editors are empty.
294     */
295    public boolean isEmpty() {
296        for (int i = 0; i < mEditors.getChildCount(); i++) {
297            View view = mEditors.getChildAt(i);
298            if (!((Editor) view).isEmpty()) {
299                return false;
300            }
301        }
302        return true;
303    }
304
305    /**
306     * Extends superclass implementation to also run tasks
307     * enqueued by {@link #runWhenWindowFocused}.
308     */
309    @Override
310    public void onWindowFocusChanged(boolean hasWindowFocus) {
311        super.onWindowFocusChanged(hasWindowFocus);
312        if (hasWindowFocus) {
313            for (Runnable r: mRunWhenWindowFocused) {
314                r.run();
315            }
316            mRunWhenWindowFocused.clear();
317        }
318    }
319
320    /**
321     * Depending on whether we are in the currently-focused window, either run
322     * the argument immediately, or stash it until our window becomes focused.
323     */
324    private void runWhenWindowFocused(Runnable r) {
325        if (hasWindowFocus()) {
326            r.run();
327        } else {
328            mRunWhenWindowFocused.add(r);
329        }
330    }
331
332    /**
333     * Simple wrapper around {@link #runWhenWindowFocused}
334     * to ensure that it runs in the UI thread.
335     */
336    private void postWhenWindowFocused(final Runnable r) {
337        post(new Runnable() {
338            @Override
339            public void run() {
340                runWhenWindowFocused(r);
341            }
342        });
343    }
344
345    public void addItem() {
346        ValuesDelta values = null;
347        // If this is a list, we can freely add. If not, only allow adding the first.
348        if (mKind.typeOverallMax == 1) {
349            if (getEditorCount() == 1) {
350                return;
351            }
352
353            // If we already have an item, just make it visible
354            ArrayList<ValuesDelta> entries = mState.getMimeEntries(mKind.mimeType);
355            if (entries != null && entries.size() > 0) {
356                values = entries.get(0);
357            }
358        }
359
360        // Insert a new child, create its view and set its focus
361        if (values == null) {
362            values = RawContactModifier.insertChild(mState, mKind);
363        }
364
365        final View newField = createEditorView(values);
366        if (newField instanceof Editor) {
367            postWhenWindowFocused(new Runnable() {
368                @Override
369                public void run() {
370                    newField.requestFocus();
371                    ((Editor)newField).editNewlyAddedField();
372                }
373            });
374        }
375
376        // Hide the "add field" footer because there is now a blank field.
377        mAddFieldFooter.setVisibility(View.GONE);
378
379        // Ensure we are visible
380        updateSectionVisible();
381    }
382
383    public int getEditorCount() {
384        return mEditors.getChildCount();
385    }
386
387    public DataKind getKind() {
388        return mKind;
389    }
390}
391