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 RawContactEditorView(Context context)87 public RawContactEditorView(Context context) { 88 super(context); 89 } 90 RawContactEditorView(Context context, AttributeSet attrs)91 public RawContactEditorView(Context context, AttributeSet attrs) { 92 super(context, attrs); 93 } 94 95 @Override setEnabled(boolean enabled)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 onFinishInflate()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 setState(EntityDelta state, AccountType type, ViewIdGenerator vig, boolean isProfile)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 addOrganizationButton.setVisibility(View.GONE); 284 organizationSectionViewContainer.setVisibility(View.VISIBLE); 285 organizationSectionViewContainer.requestFocus(); 286 } 287 }); 288 289 mFields.addView(organizationView); 290 } 291 } else { 292 // Otherwise use generic section-based editors 293 if (kind.fieldList == null) continue; 294 final KindSectionView section = (KindSectionView)mInflater.inflate( 295 R.layout.item_kind_section, mFields, false); 296 section.setEnabled(isEnabled()); 297 section.setState(kind, state, false, vig); 298 mFields.addView(section); 299 } 300 } 301 302 if (mGroupMembershipView != null) { 303 mFields.addView(mGroupMembershipView); 304 } 305 306 updatePhoneticNameVisibility(); 307 308 addToDefaultGroupIfNeeded(); 309 310 mAddFieldButton.setEnabled(isEnabled()); 311 } 312 313 @Override setGroupMetaData(Cursor groupMetaData)314 public void setGroupMetaData(Cursor groupMetaData) { 315 mGroupMetaData = groupMetaData; 316 addToDefaultGroupIfNeeded(); 317 if (mGroupMembershipView != null) { 318 mGroupMembershipView.setGroupMetaData(groupMetaData); 319 } 320 } 321 setAutoAddToDefaultGroup(boolean flag)322 public void setAutoAddToDefaultGroup(boolean flag) { 323 this.mAutoAddToDefaultGroup = flag; 324 } 325 326 /** 327 * If automatic addition to the default group was requested (see 328 * {@link #setAutoAddToDefaultGroup}, checks if the raw contact is in any 329 * group and if it is not adds it to the default group (in case of Google 330 * contacts that's "My Contacts"). 331 */ addToDefaultGroupIfNeeded()332 private void addToDefaultGroupIfNeeded() { 333 if (!mAutoAddToDefaultGroup || mGroupMetaData == null || mGroupMetaData.isClosed() 334 || mState == null) { 335 return; 336 } 337 338 boolean hasGroupMembership = false; 339 ArrayList<ValuesDelta> entries = mState.getMimeEntries(GroupMembership.CONTENT_ITEM_TYPE); 340 if (entries != null) { 341 for (ValuesDelta values : entries) { 342 Long id = values.getAsLong(GroupMembership.GROUP_ROW_ID); 343 if (id != null && id.longValue() != 0) { 344 hasGroupMembership = true; 345 break; 346 } 347 } 348 } 349 350 if (!hasGroupMembership) { 351 long defaultGroupId = getDefaultGroupId(); 352 if (defaultGroupId != -1) { 353 ValuesDelta entry = EntityModifier.insertChild(mState, mGroupMembershipKind); 354 entry.put(GroupMembership.GROUP_ROW_ID, defaultGroupId); 355 } 356 } 357 } 358 359 /** 360 * Returns the default group (e.g. "My Contacts") for the current raw contact's 361 * account. Returns -1 if there is no such group. 362 */ getDefaultGroupId()363 private long getDefaultGroupId() { 364 String accountType = mState.getValues().getAsString(RawContacts.ACCOUNT_TYPE); 365 String accountName = mState.getValues().getAsString(RawContacts.ACCOUNT_NAME); 366 String accountDataSet = mState.getValues().getAsString(RawContacts.DATA_SET); 367 mGroupMetaData.moveToPosition(-1); 368 while (mGroupMetaData.moveToNext()) { 369 String name = mGroupMetaData.getString(GroupMetaDataLoader.ACCOUNT_NAME); 370 String type = mGroupMetaData.getString(GroupMetaDataLoader.ACCOUNT_TYPE); 371 String dataSet = mGroupMetaData.getString(GroupMetaDataLoader.DATA_SET); 372 if (name.equals(accountName) && type.equals(accountType) 373 && Objects.equal(dataSet, accountDataSet)) { 374 long groupId = mGroupMetaData.getLong(GroupMetaDataLoader.GROUP_ID); 375 if (!mGroupMetaData.isNull(GroupMetaDataLoader.AUTO_ADD) 376 && mGroupMetaData.getInt(GroupMetaDataLoader.AUTO_ADD) != 0) { 377 return groupId; 378 } 379 } 380 } 381 return -1; 382 } 383 getNameEditor()384 public TextFieldsEditorView getNameEditor() { 385 return mName; 386 } 387 getPhoneticNameEditor()388 public TextFieldsEditorView getPhoneticNameEditor() { 389 return mPhoneticName; 390 } 391 updatePhoneticNameVisibility()392 private void updatePhoneticNameVisibility() { 393 boolean showByDefault = 394 getContext().getResources().getBoolean(R.bool.config_editor_include_phonetic_name); 395 396 if (showByDefault || mPhoneticName.hasData() || mPhoneticNameAdded) { 397 mPhoneticName.setVisibility(View.VISIBLE); 398 } else { 399 mPhoneticName.setVisibility(View.GONE); 400 } 401 } 402 403 @Override getRawContactId()404 public long getRawContactId() { 405 return mRawContactId; 406 } 407 showAddInformationPopupWindow()408 private void showAddInformationPopupWindow() { 409 final ArrayList<KindSectionView> fields = 410 new ArrayList<KindSectionView>(mFields.getChildCount()); 411 412 final PopupMenu popupMenu = new PopupMenu(getContext(), mAddFieldButton); 413 final Menu menu = popupMenu.getMenu(); 414 for (int i = 0; i < mFields.getChildCount(); i++) { 415 View child = mFields.getChildAt(i); 416 if (child instanceof KindSectionView) { 417 final KindSectionView sectionView = (KindSectionView) child; 418 // If the section is already visible (has 1 or more editors), then don't offer the 419 // option to add this type of field in the popup menu 420 if (sectionView.getEditorCount() > 0) { 421 continue; 422 } 423 DataKind kind = sectionView.getKind(); 424 // not a list and already exists? ignore 425 if ((kind.typeOverallMax == 1) && sectionView.getEditorCount() != 0) { 426 continue; 427 } 428 if (DataKind.PSEUDO_MIME_TYPE_DISPLAY_NAME.equals(kind.mimeType)) { 429 continue; 430 } 431 432 if (DataKind.PSEUDO_MIME_TYPE_PHONETIC_NAME.equals(kind.mimeType) 433 && mPhoneticName.getVisibility() == View.VISIBLE) { 434 continue; 435 } 436 437 menu.add(Menu.NONE, fields.size(), Menu.NONE, sectionView.getTitle()); 438 fields.add(sectionView); 439 } 440 } 441 442 popupMenu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() { 443 @Override 444 public boolean onMenuItemClick(MenuItem item) { 445 final KindSectionView view = fields.get(item.getItemId()); 446 if (DataKind.PSEUDO_MIME_TYPE_PHONETIC_NAME.equals(view.getKind().mimeType)) { 447 mPhoneticNameAdded = true; 448 updatePhoneticNameVisibility(); 449 } else { 450 view.addItem(); 451 } 452 return true; 453 } 454 }); 455 456 popupMenu.show(); 457 } 458 } 459