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