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