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.database.Cursor;
21import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
22import android.provider.ContactsContract.CommonDataKinds.Organization;
23import android.provider.ContactsContract.CommonDataKinds.Photo;
24import android.provider.ContactsContract.CommonDataKinds.StructuredName;
25import android.provider.ContactsContract.Contacts;
26import android.provider.ContactsContract.Data;
27import android.text.TextUtils;
28import android.util.AttributeSet;
29import android.view.LayoutInflater;
30import android.view.Menu;
31import android.view.MenuItem;
32import android.view.View;
33import android.view.ViewGroup;
34import android.widget.Button;
35import android.widget.ImageView;
36import android.widget.PopupMenu;
37import android.widget.TextView;
38
39import com.android.contacts.GroupMetaDataLoader;
40import com.android.contacts.R;
41import com.android.contacts.model.RawContactDelta;
42import com.android.contacts.model.RawContactDelta.ValuesDelta;
43import com.android.contacts.model.RawContactModifier;
44import com.android.contacts.model.account.AccountType;
45import com.android.contacts.model.account.AccountType.EditType;
46import com.android.contacts.model.dataitem.DataKind;
47import com.android.internal.util.Objects;
48
49import java.util.ArrayList;
50
51/**
52 * Custom view that provides all the editor interaction for a specific
53 * {@link Contacts} represented through an {@link RawContactDelta}. Callers can
54 * reuse this view and quickly rebuild its contents through
55 * {@link #setState(RawContactDelta, AccountType, ViewIdGenerator)}.
56 * <p>
57 * Internal updates are performed against {@link ValuesDelta} so that the
58 * source {@link RawContact} can be swapped out. Any state-based changes, such as
59 * adding {@link Data} rows or changing {@link EditType}, are performed through
60 * {@link RawContactModifier} to ensure that {@link AccountType} are enforced.
61 */
62public class RawContactEditorView extends BaseRawContactEditorView {
63    private LayoutInflater mInflater;
64
65    private StructuredNameEditorView mName;
66    private PhoneticNameEditorView mPhoneticName;
67    private GroupMembershipView mGroupMembershipView;
68
69    private ViewGroup mFields;
70
71    private ImageView mAccountIcon;
72    private TextView mAccountTypeTextView;
73    private TextView mAccountNameTextView;
74
75    private Button mAddFieldButton;
76
77    private long mRawContactId = -1;
78    private boolean mAutoAddToDefaultGroup = true;
79    private Cursor mGroupMetaData;
80    private DataKind mGroupMembershipKind;
81    private RawContactDelta mState;
82
83    private boolean mPhoneticNameAdded;
84
85    public RawContactEditorView(Context context) {
86        super(context);
87    }
88
89    public RawContactEditorView(Context context, AttributeSet attrs) {
90        super(context, attrs);
91    }
92
93    @Override
94    public void setEnabled(boolean enabled) {
95        super.setEnabled(enabled);
96
97        View view = getPhotoEditor();
98        if (view != null) {
99            view.setEnabled(enabled);
100        }
101
102        if (mName != null) {
103            mName.setEnabled(enabled);
104        }
105
106        if (mPhoneticName != null) {
107            mPhoneticName.setEnabled(enabled);
108        }
109
110        if (mFields != null) {
111            int count = mFields.getChildCount();
112            for (int i = 0; i < count; i++) {
113                mFields.getChildAt(i).setEnabled(enabled);
114            }
115        }
116
117        if (mGroupMembershipView != null) {
118            mGroupMembershipView.setEnabled(enabled);
119        }
120
121        mAddFieldButton.setEnabled(enabled);
122    }
123
124    @Override
125    protected void onFinishInflate() {
126        super.onFinishInflate();
127
128        mInflater = (LayoutInflater)getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
129
130        mName = (StructuredNameEditorView)findViewById(R.id.edit_name);
131        mName.setDeletable(false);
132
133        mPhoneticName = (PhoneticNameEditorView)findViewById(R.id.edit_phonetic_name);
134        mPhoneticName.setDeletable(false);
135
136        mFields = (ViewGroup)findViewById(R.id.sect_fields);
137
138        mAccountIcon = (ImageView) findViewById(R.id.account_icon);
139        mAccountTypeTextView = (TextView) findViewById(R.id.account_type);
140        mAccountNameTextView = (TextView) findViewById(R.id.account_name);
141
142        mAddFieldButton = (Button) findViewById(R.id.button_add_field);
143        mAddFieldButton.setOnClickListener(new OnClickListener() {
144            @Override
145            public void onClick(View v) {
146                showAddInformationPopupWindow();
147            }
148        });
149    }
150
151    /**
152     * Set the internal state for this view, given a current
153     * {@link RawContactDelta} state and the {@link AccountType} that
154     * apply to that state.
155     */
156    @Override
157    public void setState(RawContactDelta state, AccountType type, ViewIdGenerator vig,
158            boolean isProfile) {
159
160        mState = state;
161
162        // Remove any existing sections
163        mFields.removeAllViews();
164
165        // Bail if invalid state or account type
166        if (state == null || type == null) return;
167
168        setId(vig.getId(state, null, null, ViewIdGenerator.NO_VIEW_INDEX));
169
170        // Make sure we have a StructuredName and Organization
171        RawContactModifier.ensureKindExists(state, type, StructuredName.CONTENT_ITEM_TYPE);
172        RawContactModifier.ensureKindExists(state, type, Organization.CONTENT_ITEM_TYPE);
173
174        mRawContactId = state.getRawContactId();
175
176        // Fill in the account info
177        if (isProfile) {
178            String accountName = state.getAccountName();
179            if (TextUtils.isEmpty(accountName)) {
180                mAccountNameTextView.setVisibility(View.GONE);
181                mAccountTypeTextView.setText(R.string.local_profile_title);
182            } else {
183                CharSequence accountType = type.getDisplayLabel(mContext);
184                mAccountTypeTextView.setText(mContext.getString(R.string.external_profile_title,
185                        accountType));
186                mAccountNameTextView.setText(accountName);
187            }
188        } else {
189            String accountName = state.getAccountName();
190            CharSequence accountType = type.getDisplayLabel(mContext);
191            if (TextUtils.isEmpty(accountType)) {
192                accountType = mContext.getString(R.string.account_phone);
193            }
194            if (!TextUtils.isEmpty(accountName)) {
195                mAccountNameTextView.setVisibility(View.VISIBLE);
196                mAccountNameTextView.setText(
197                        mContext.getString(R.string.from_account_format, accountName));
198            } else {
199                // Hide this view so the other text view will be centered vertically
200                mAccountNameTextView.setVisibility(View.GONE);
201            }
202            mAccountTypeTextView.setText(
203                    mContext.getString(R.string.account_type_format, accountType));
204        }
205        mAccountIcon.setImageDrawable(type.getDisplayIcon(mContext));
206
207        // Show photo editor when supported
208        RawContactModifier.ensureKindExists(state, type, Photo.CONTENT_ITEM_TYPE);
209        setHasPhotoEditor((type.getKindForMimetype(Photo.CONTENT_ITEM_TYPE) != null));
210        getPhotoEditor().setEnabled(isEnabled());
211        mName.setEnabled(isEnabled());
212
213        mPhoneticName.setEnabled(isEnabled());
214
215        // Show and hide the appropriate views
216        mFields.setVisibility(View.VISIBLE);
217        mName.setVisibility(View.VISIBLE);
218        mPhoneticName.setVisibility(View.VISIBLE);
219
220        mGroupMembershipKind = type.getKindForMimetype(GroupMembership.CONTENT_ITEM_TYPE);
221        if (mGroupMembershipKind != null) {
222            mGroupMembershipView = (GroupMembershipView)mInflater.inflate(
223                    R.layout.item_group_membership, mFields, false);
224            mGroupMembershipView.setKind(mGroupMembershipKind);
225            mGroupMembershipView.setEnabled(isEnabled());
226        }
227
228        // Create editor sections for each possible data kind
229        for (DataKind kind : type.getSortedDataKinds()) {
230            // Skip kind of not editable
231            if (!kind.editable) continue;
232
233            final String mimeType = kind.mimeType;
234            if (StructuredName.CONTENT_ITEM_TYPE.equals(mimeType)) {
235                // Handle special case editor for structured name
236                final ValuesDelta primary = state.getPrimaryEntry(mimeType);
237                mName.setValues(
238                        type.getKindForMimetype(DataKind.PSEUDO_MIME_TYPE_DISPLAY_NAME),
239                        primary, state, false, vig);
240                mPhoneticName.setValues(
241                        type.getKindForMimetype(DataKind.PSEUDO_MIME_TYPE_PHONETIC_NAME),
242                        primary, state, false, vig);
243            } else if (Photo.CONTENT_ITEM_TYPE.equals(mimeType)) {
244                // Handle special case editor for photos
245                final ValuesDelta primary = state.getPrimaryEntry(mimeType);
246                getPhotoEditor().setValues(kind, primary, state, false, vig);
247            } else if (GroupMembership.CONTENT_ITEM_TYPE.equals(mimeType)) {
248                if (mGroupMembershipView != null) {
249                    mGroupMembershipView.setState(state);
250                }
251            } else if (Organization.CONTENT_ITEM_TYPE.equals(mimeType)) {
252                // Create the organization section
253                final KindSectionView section = (KindSectionView) mInflater.inflate(
254                        R.layout.item_kind_section, mFields, false);
255                section.setTitleVisible(false);
256                section.setEnabled(isEnabled());
257                section.setState(kind, state, false, vig);
258
259                // If there is organization info for the contact already, display it
260                if (!section.isEmpty()) {
261                    mFields.addView(section);
262                } else {
263                    // Otherwise provide the user with an "add organization" button that shows the
264                    // EditText fields only when clicked
265                    final View organizationView = mInflater.inflate(
266                            R.layout.organization_editor_view_switcher, mFields, false);
267                    final View addOrganizationButton = organizationView.findViewById(
268                            R.id.add_organization_button);
269                    final ViewGroup organizationSectionViewContainer =
270                            (ViewGroup) organizationView.findViewById(R.id.container);
271
272                    organizationSectionViewContainer.addView(section);
273
274                    // Setup the click listener for the "add organization" button
275                    addOrganizationButton.setOnClickListener(new OnClickListener() {
276                        @Override
277                        public void onClick(View v) {
278                            // Once the user expands the organization field, the user cannot
279                            // collapse them again.
280                            EditorAnimator.getInstance().expandOrganization(addOrganizationButton,
281                                    organizationSectionViewContainer);
282                        }
283                    });
284
285                    mFields.addView(organizationView);
286                }
287            } else {
288                // Otherwise use generic section-based editors
289                if (kind.fieldList == null) continue;
290                final KindSectionView section = (KindSectionView)mInflater.inflate(
291                        R.layout.item_kind_section, mFields, false);
292                section.setEnabled(isEnabled());
293                section.setState(kind, state, false, vig);
294                mFields.addView(section);
295            }
296        }
297
298        if (mGroupMembershipView != null) {
299            mFields.addView(mGroupMembershipView);
300        }
301
302        updatePhoneticNameVisibility();
303
304        addToDefaultGroupIfNeeded();
305
306
307        final int sectionCount = getSectionViewsWithoutFields().size();
308        mAddFieldButton.setVisibility(sectionCount > 0 ? View.VISIBLE : View.GONE);
309        mAddFieldButton.setEnabled(isEnabled());
310    }
311
312    @Override
313    public void setGroupMetaData(Cursor groupMetaData) {
314        mGroupMetaData = groupMetaData;
315        addToDefaultGroupIfNeeded();
316        if (mGroupMembershipView != null) {
317            mGroupMembershipView.setGroupMetaData(groupMetaData);
318        }
319    }
320
321    public void setAutoAddToDefaultGroup(boolean flag) {
322        this.mAutoAddToDefaultGroup = flag;
323    }
324
325    /**
326     * If automatic addition to the default group was requested (see
327     * {@link #setAutoAddToDefaultGroup}, checks if the raw contact is in any
328     * group and if it is not adds it to the default group (in case of Google
329     * contacts that's "My Contacts").
330     */
331    private void addToDefaultGroupIfNeeded() {
332        if (!mAutoAddToDefaultGroup || mGroupMetaData == null || mGroupMetaData.isClosed()
333                || mState == null) {
334            return;
335        }
336
337        boolean hasGroupMembership = false;
338        ArrayList<ValuesDelta> entries = mState.getMimeEntries(GroupMembership.CONTENT_ITEM_TYPE);
339        if (entries != null) {
340            for (ValuesDelta values : entries) {
341                Long id = values.getGroupRowId();
342                if (id != null && id.longValue() != 0) {
343                    hasGroupMembership = true;
344                    break;
345                }
346            }
347        }
348
349        if (!hasGroupMembership) {
350            long defaultGroupId = getDefaultGroupId();
351            if (defaultGroupId != -1) {
352                ValuesDelta entry = RawContactModifier.insertChild(mState, mGroupMembershipKind);
353                entry.setGroupRowId(defaultGroupId);
354            }
355        }
356    }
357
358    /**
359     * Returns the default group (e.g. "My Contacts") for the current raw contact's
360     * account.  Returns -1 if there is no such group.
361     */
362    private long getDefaultGroupId() {
363        String accountType = mState.getAccountType();
364        String accountName = mState.getAccountName();
365        String accountDataSet = mState.getDataSet();
366        mGroupMetaData.moveToPosition(-1);
367        while (mGroupMetaData.moveToNext()) {
368            String name = mGroupMetaData.getString(GroupMetaDataLoader.ACCOUNT_NAME);
369            String type = mGroupMetaData.getString(GroupMetaDataLoader.ACCOUNT_TYPE);
370            String dataSet = mGroupMetaData.getString(GroupMetaDataLoader.DATA_SET);
371            if (name.equals(accountName) && type.equals(accountType)
372                    && Objects.equal(dataSet, accountDataSet)) {
373                long groupId = mGroupMetaData.getLong(GroupMetaDataLoader.GROUP_ID);
374                if (!mGroupMetaData.isNull(GroupMetaDataLoader.AUTO_ADD)
375                            && mGroupMetaData.getInt(GroupMetaDataLoader.AUTO_ADD) != 0) {
376                    return groupId;
377                }
378            }
379        }
380        return -1;
381    }
382
383    public TextFieldsEditorView getNameEditor() {
384        return mName;
385    }
386
387    public TextFieldsEditorView getPhoneticNameEditor() {
388        return mPhoneticName;
389    }
390
391    private void updatePhoneticNameVisibility() {
392        boolean showByDefault =
393                getContext().getResources().getBoolean(R.bool.config_editor_include_phonetic_name);
394
395        if (showByDefault || mPhoneticName.hasData() || mPhoneticNameAdded) {
396            mPhoneticName.setVisibility(View.VISIBLE);
397        } else {
398            mPhoneticName.setVisibility(View.GONE);
399        }
400    }
401
402    @Override
403    public long getRawContactId() {
404        return mRawContactId;
405    }
406
407    /**
408     * Return a list of KindSectionViews that have no fields yet...
409     * these are candidates to have fields added in
410     * {@link #showAddInformationPopupWindow()}
411     */
412    private ArrayList<KindSectionView> getSectionViewsWithoutFields() {
413        final ArrayList<KindSectionView> fields =
414                new ArrayList<KindSectionView>(mFields.getChildCount());
415        for (int i = 0; i < mFields.getChildCount(); i++) {
416            View child = mFields.getChildAt(i);
417            if (child instanceof KindSectionView) {
418                final KindSectionView sectionView = (KindSectionView) child;
419                // If the section is already visible (has 1 or more editors), then don't offer the
420                // option to add this type of field in the popup menu
421                if (sectionView.getEditorCount() > 0) {
422                    continue;
423                }
424                DataKind kind = sectionView.getKind();
425                // not a list and already exists? ignore
426                if ((kind.typeOverallMax == 1) && sectionView.getEditorCount() != 0) {
427                    continue;
428                }
429                if (DataKind.PSEUDO_MIME_TYPE_DISPLAY_NAME.equals(kind.mimeType)) {
430                    continue;
431                }
432
433                if (DataKind.PSEUDO_MIME_TYPE_PHONETIC_NAME.equals(kind.mimeType)
434                        && mPhoneticName.getVisibility() == View.VISIBLE) {
435                    continue;
436                }
437
438                fields.add(sectionView);
439            }
440        }
441        return fields;
442    }
443
444    private void showAddInformationPopupWindow() {
445        final ArrayList<KindSectionView> fields = getSectionViewsWithoutFields();
446        final PopupMenu popupMenu = new PopupMenu(getContext(), mAddFieldButton);
447        final Menu menu = popupMenu.getMenu();
448        for (int i = 0; i < fields.size(); i++) {
449            menu.add(Menu.NONE, i, Menu.NONE, fields.get(i).getTitle());
450        }
451
452        popupMenu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
453            @Override
454            public boolean onMenuItemClick(MenuItem item) {
455                final KindSectionView view = fields.get(item.getItemId());
456                if (DataKind.PSEUDO_MIME_TYPE_PHONETIC_NAME.equals(view.getKind().mimeType)) {
457                    mPhoneticNameAdded = true;
458                    updatePhoneticNameVisibility();
459                } else {
460                    view.addItem();
461                }
462
463                // If this was the last section without an entry, we just added one, and therefore
464                // there's no reason to show the button.
465                if (fields.size() == 1) {
466                    mAddFieldButton.setVisibility(View.GONE);
467                }
468
469                return true;
470            }
471        });
472
473        popupMenu.show();
474    }
475}
476