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.common.model.RawContactDelta;
47import com.android.contacts.common.model.ValuesDelta;
48import com.android.contacts.common.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                if (entry != null) {
395                    entry.setGroupRowId(defaultGroupId);
396                }
397            }
398        }
399    }
400
401    /**
402     * Returns the default group (e.g. "My Contacts") for the current raw contact's
403     * account.  Returns -1 if there is no such group.
404     */
405    private long getDefaultGroupId() {
406        String accountType = mState.getAccountType();
407        String accountName = mState.getAccountName();
408        String accountDataSet = mState.getDataSet();
409        mGroupMetaData.moveToPosition(-1);
410        while (mGroupMetaData.moveToNext()) {
411            String name = mGroupMetaData.getString(GroupMetaDataLoader.ACCOUNT_NAME);
412            String type = mGroupMetaData.getString(GroupMetaDataLoader.ACCOUNT_TYPE);
413            String dataSet = mGroupMetaData.getString(GroupMetaDataLoader.DATA_SET);
414            if (name.equals(accountName) && type.equals(accountType)
415                    && Objects.equal(dataSet, accountDataSet)) {
416                long groupId = mGroupMetaData.getLong(GroupMetaDataLoader.GROUP_ID);
417                if (!mGroupMetaData.isNull(GroupMetaDataLoader.AUTO_ADD)
418                            && mGroupMetaData.getInt(GroupMetaDataLoader.AUTO_ADD) != 0) {
419                    return groupId;
420                }
421            }
422        }
423        return -1;
424    }
425
426    public StructuredNameEditorView getNameEditor() {
427        return mName;
428    }
429
430    public TextFieldsEditorView getPhoneticNameEditor() {
431        return mPhoneticName;
432    }
433
434    private void updatePhoneticNameVisibility() {
435        boolean showByDefault =
436                getContext().getResources().getBoolean(R.bool.config_editor_include_phonetic_name);
437
438        if (showByDefault || mPhoneticName.hasData() || mPhoneticNameAdded) {
439            mPhoneticName.setVisibility(View.VISIBLE);
440        } else {
441            mPhoneticName.setVisibility(View.GONE);
442        }
443    }
444
445    @Override
446    public long getRawContactId() {
447        return mRawContactId;
448    }
449
450    /**
451     * Return a list of KindSectionViews that have no fields yet...
452     * these are candidates to have fields added in
453     * {@link #showAddInformationPopupWindow()}
454     */
455    private ArrayList<KindSectionView> getSectionViewsWithoutFields() {
456        final ArrayList<KindSectionView> fields =
457                new ArrayList<KindSectionView>(mFields.getChildCount());
458        for (int i = 0; i < mFields.getChildCount(); i++) {
459            View child = mFields.getChildAt(i);
460            if (child instanceof KindSectionView) {
461                final KindSectionView sectionView = (KindSectionView) child;
462                // If the section is already visible (has 1 or more editors), then don't offer the
463                // option to add this type of field in the popup menu
464                if (sectionView.getEditorCount() > 0) {
465                    continue;
466                }
467                DataKind kind = sectionView.getKind();
468                // not a list and already exists? ignore
469                if ((kind.typeOverallMax == 1) && sectionView.getEditorCount() != 0) {
470                    continue;
471                }
472                if (DataKind.PSEUDO_MIME_TYPE_DISPLAY_NAME.equals(kind.mimeType)) {
473                    continue;
474                }
475
476                if (DataKind.PSEUDO_MIME_TYPE_PHONETIC_NAME.equals(kind.mimeType)
477                        && mPhoneticName.getVisibility() == View.VISIBLE) {
478                    continue;
479                }
480
481                fields.add(sectionView);
482            }
483        }
484        return fields;
485    }
486
487    private void showAddInformationPopupWindow() {
488        final ArrayList<KindSectionView> fields = getSectionViewsWithoutFields();
489        final PopupMenu popupMenu = new PopupMenu(getContext(), mAddFieldButton);
490        final Menu menu = popupMenu.getMenu();
491        for (int i = 0; i < fields.size(); i++) {
492            menu.add(Menu.NONE, i, Menu.NONE, fields.get(i).getTitle());
493        }
494
495        popupMenu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
496            @Override
497            public boolean onMenuItemClick(MenuItem item) {
498                final KindSectionView view = fields.get(item.getItemId());
499                if (DataKind.PSEUDO_MIME_TYPE_PHONETIC_NAME.equals(view.getKind().mimeType)) {
500                    mPhoneticNameAdded = true;
501                    updatePhoneticNameVisibility();
502                } else {
503                    view.addItem();
504                }
505
506                // If this was the last section without an entry, we just added one, and therefore
507                // there's no reason to show the button.
508                if (fields.size() == 1) {
509                    mAddFieldButton.setVisibility(View.GONE);
510                }
511
512                return true;
513            }
514        });
515
516        popupMenu.show();
517    }
518}
519