Home | History | Annotate | Download | only in editor
      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 
     17 package com.android.contacts.editor;
     18 
     19 import android.content.Context;
     20 import android.database.Cursor;
     21 import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
     22 import android.provider.ContactsContract.CommonDataKinds.Organization;
     23 import android.provider.ContactsContract.CommonDataKinds.Photo;
     24 import android.provider.ContactsContract.CommonDataKinds.StructuredName;
     25 import android.provider.ContactsContract.Contacts;
     26 import android.provider.ContactsContract.Data;
     27 import android.text.TextUtils;
     28 import android.util.AttributeSet;
     29 import android.view.LayoutInflater;
     30 import android.view.Menu;
     31 import android.view.MenuItem;
     32 import android.view.View;
     33 import android.view.ViewGroup;
     34 import android.widget.Button;
     35 import android.widget.ImageView;
     36 import android.widget.PopupMenu;
     37 import android.widget.TextView;
     38 
     39 import com.android.contacts.GroupMetaDataLoader;
     40 import com.android.contacts.R;
     41 import com.android.contacts.model.RawContactDelta;
     42 import com.android.contacts.model.RawContactDelta.ValuesDelta;
     43 import com.android.contacts.model.RawContactModifier;
     44 import com.android.contacts.model.account.AccountType;
     45 import com.android.contacts.model.account.AccountType.EditType;
     46 import com.android.contacts.model.dataitem.DataKind;
     47 import com.android.internal.util.Objects;
     48 
     49 import 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  */
     62 public 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