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.common.model.RawContactDelta; 47 import com.android.contacts.common.model.ValuesDelta; 48 import com.android.contacts.common.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