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