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.os.Bundle;
     22 import android.os.Parcelable;
     23 import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
     24 import android.provider.ContactsContract.CommonDataKinds.Organization;
     25 import android.provider.ContactsContract.CommonDataKinds.Photo;
     26 import android.provider.ContactsContract.CommonDataKinds.StructuredName;
     27 import android.provider.ContactsContract.Contacts;
     28 import android.provider.ContactsContract.Data;
     29 import android.text.TextUtils;
     30 import android.util.AttributeSet;
     31 import android.view.LayoutInflater;
     32 import android.view.Menu;
     33 import android.view.MenuItem;
     34 import android.view.View;
     35 import android.view.ViewGroup;
     36 import android.widget.Button;
     37 import android.widget.ImageView;
     38 import android.widget.PopupMenu;
     39 import android.widget.TextView;
     40 
     41 import com.android.contacts.GroupMetaDataLoader;
     42 import com.android.contacts.R;
     43 import com.android.contacts.common.model.account.AccountType;
     44 import com.android.contacts.common.model.account.AccountType.EditType;
     45 import com.android.contacts.common.model.dataitem.DataKind;
     46 import com.android.contacts.model.RawContactDelta;
     47 import com.android.contacts.common.model.ValuesDelta;
     48 import com.android.contacts.model.RawContactModifier;
     49 import com.google.common.base.Objects;
     50 
     51 import 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  */
     64 public 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