RawContactEditorView.java revision ac5bd644bcd2c27294a70774abfa9e24fb3d5c52
1/* 2 * Copyright (C) 2015 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 17package com.android.contacts.editor; 18 19import android.content.Context; 20import android.content.res.Resources; 21import android.database.Cursor; 22import android.graphics.drawable.Drawable; 23import android.net.Uri; 24import android.os.Parcel; 25import android.os.Parcelable; 26import android.provider.ContactsContract.CommonDataKinds.Email; 27import android.provider.ContactsContract.CommonDataKinds.Event; 28import android.provider.ContactsContract.CommonDataKinds.GroupMembership; 29import android.provider.ContactsContract.CommonDataKinds.Im; 30import android.provider.ContactsContract.CommonDataKinds.Nickname; 31import android.provider.ContactsContract.CommonDataKinds.Note; 32import android.provider.ContactsContract.CommonDataKinds.Organization; 33import android.provider.ContactsContract.CommonDataKinds.Phone; 34import android.provider.ContactsContract.CommonDataKinds.Photo; 35import android.provider.ContactsContract.CommonDataKinds.Relation; 36import android.provider.ContactsContract.CommonDataKinds.SipAddress; 37import android.provider.ContactsContract.CommonDataKinds.StructuredName; 38import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; 39import android.provider.ContactsContract.CommonDataKinds.Website; 40import android.text.TextUtils; 41import android.util.AttributeSet; 42import android.util.Log; 43import android.view.LayoutInflater; 44import android.view.View; 45import android.view.ViewGroup; 46import android.widget.AdapterView; 47import android.widget.ImageView; 48import android.widget.LinearLayout; 49import android.widget.ListPopupWindow; 50import android.widget.TextView; 51 52import com.android.contacts.R; 53import com.android.contacts.common.GeoUtil; 54import com.android.contacts.common.compat.PhoneNumberUtilsCompat; 55import com.android.contacts.common.model.AccountTypeManager; 56import com.android.contacts.common.model.RawContactDelta; 57import com.android.contacts.common.model.RawContactDeltaList; 58import com.android.contacts.common.model.RawContactModifier; 59import com.android.contacts.common.model.ValuesDelta; 60import com.android.contacts.common.model.account.AccountDisplayInfo; 61import com.android.contacts.common.model.account.AccountDisplayInfoFactory; 62import com.android.contacts.common.model.account.AccountType; 63import com.android.contacts.common.model.account.AccountWithDataSet; 64import com.android.contacts.common.model.dataitem.CustomDataItem; 65import com.android.contacts.common.model.dataitem.DataKind; 66import com.android.contacts.common.util.AccountsListAdapter; 67import com.android.contacts.common.util.MaterialColorMapUtils; 68import com.android.contacts.util.UiClosables; 69 70import java.io.FileNotFoundException; 71import java.util.ArrayList; 72import java.util.Arrays; 73import java.util.Comparator; 74import java.util.HashMap; 75import java.util.List; 76import java.util.Map; 77import java.util.Set; 78import java.util.TreeSet; 79 80/** 81 * View to display information from multiple {@link RawContactDelta}s grouped together. 82 */ 83public class RawContactEditorView extends LinearLayout implements View.OnClickListener { 84 85 static final String TAG = "RawContactEditorView"; 86 87 /** 88 * Callbacks for hosts of {@link RawContactEditorView}s. 89 */ 90 public interface Listener { 91 92 /** 93 * Invoked when the structured name editor field has changed. 94 * 95 * @param rawContactId The raw contact ID from the underlying {@link RawContactDelta}. 96 * @param valuesDelta The values from the underlying {@link RawContactDelta}. 97 */ 98 public void onNameFieldChanged(long rawContactId, ValuesDelta valuesDelta); 99 100 /** 101 * Invoked when the editor should rebind editors for a new account. 102 * 103 * @param oldState Old data being edited. 104 * @param oldAccount Old account associated with oldState. 105 * @param newAccount New account to be used. 106 */ 107 public void onRebindEditorsForNewContact(RawContactDelta oldState, 108 AccountWithDataSet oldAccount, AccountWithDataSet newAccount); 109 110 /** 111 * Invoked when no editors could be bound for the contact. 112 */ 113 public void onBindEditorsFailed(); 114 115 /** 116 * Invoked after editors have been bound for the contact. 117 */ 118 public void onEditorsBound(); 119 } 120 /** 121 * Sorts kinds roughly the same as quick contacts; we diverge in the following ways: 122 * <ol> 123 * <li>All names are together at the top.</li> 124 * <li>IM is moved up after addresses</li> 125 * <li>SIP addresses are moved to below phone numbers</li> 126 * <li>Group membership is placed at the end</li> 127 * </ol> 128 */ 129 private static final class MimeTypeComparator implements Comparator<String> { 130 131 private static final List<String> MIME_TYPE_ORDER = Arrays.asList(new String[] { 132 StructuredName.CONTENT_ITEM_TYPE, 133 Nickname.CONTENT_ITEM_TYPE, 134 Organization.CONTENT_ITEM_TYPE, 135 Phone.CONTENT_ITEM_TYPE, 136 SipAddress.CONTENT_ITEM_TYPE, 137 Email.CONTENT_ITEM_TYPE, 138 StructuredPostal.CONTENT_ITEM_TYPE, 139 Im.CONTENT_ITEM_TYPE, 140 Website.CONTENT_ITEM_TYPE, 141 Event.CONTENT_ITEM_TYPE, 142 Relation.CONTENT_ITEM_TYPE, 143 Note.CONTENT_ITEM_TYPE, 144 GroupMembership.CONTENT_ITEM_TYPE 145 }); 146 147 @Override 148 public int compare(String mimeType1, String mimeType2) { 149 if (mimeType1 == mimeType2) return 0; 150 if (mimeType1 == null) return -1; 151 if (mimeType2 == null) return 1; 152 153 int index1 = MIME_TYPE_ORDER.indexOf(mimeType1); 154 int index2 = MIME_TYPE_ORDER.indexOf(mimeType2); 155 156 // Fallback to alphabetical ordering of the mime type if both are not found 157 if (index1 < 0 && index2 < 0) return mimeType1.compareTo(mimeType2); 158 if (index1 < 0) return 1; 159 if (index2 < 0) return -1; 160 161 return index1 < index2 ? -1 : 1; 162 } 163 } 164 165 public static class SavedState extends BaseSavedState { 166 167 public static final Parcelable.Creator<SavedState> CREATOR = 168 new Parcelable.Creator<SavedState>() { 169 public SavedState createFromParcel(Parcel in) { 170 return new SavedState(in); 171 } 172 public SavedState[] newArray(int size) { 173 return new SavedState[size]; 174 } 175 }; 176 177 private boolean mIsExpanded; 178 179 public SavedState(Parcelable superState) { 180 super(superState); 181 } 182 183 private SavedState(Parcel in) { 184 super(in); 185 mIsExpanded = in.readInt() != 0; 186 } 187 188 @Override 189 public void writeToParcel(Parcel out, int flags) { 190 super.writeToParcel(out, flags); 191 out.writeInt(mIsExpanded ? 1 : 0); 192 } 193 } 194 195 private RawContactEditorView.Listener mListener; 196 197 private AccountTypeManager mAccountTypeManager; 198 private AccountDisplayInfoFactory mAccountDisplayInfoFactory; 199 private LayoutInflater mLayoutInflater; 200 201 private ViewIdGenerator mViewIdGenerator; 202 private MaterialColorMapUtils.MaterialPalette mMaterialPalette; 203 private boolean mHasNewContact; 204 private boolean mIsUserProfile; 205 private AccountWithDataSet mPrimaryAccount; 206 private RawContactDeltaList mRawContactDeltas; 207 private RawContactDelta mCurrentRawContactDelta; 208 private long mRawContactIdToDisplayAlone = -1; 209 private boolean mIsEditingReadOnlyRawContactWithNewContact; 210 private Map<String, KindSectionData> mKindSectionDataMap = new HashMap<>(); 211 private Set<String> mSortedMimetypes = new TreeSet<>(new MimeTypeComparator()); 212 213 // Account header 214 private View mAccountHeaderContainer; 215 private TextView mAccountHeaderType; 216 private TextView mAccountHeaderName; 217 private ImageView mAccountHeaderIcon; 218 private ImageView mAccountHeaderExpanderIcon; 219 220 private PhotoEditorView mPhotoView; 221 private ViewGroup mKindSectionViews; 222 private Map<String, KindSectionView> mKindSectionViewMap = new HashMap<>(); 223 private View mMoreFields; 224 225 private boolean mIsExpanded; 226 227 private ValuesDelta mPhotoValuesDelta; 228 229 public RawContactEditorView(Context context) { 230 super(context); 231 } 232 233 public RawContactEditorView(Context context, AttributeSet attrs) { 234 super(context, attrs); 235 } 236 237 /** 238 * Sets the receiver for {@link RawContactEditorView} callbacks. 239 */ 240 public void setListener(Listener listener) { 241 mListener = listener; 242 } 243 244 @Override 245 protected void onFinishInflate() { 246 super.onFinishInflate(); 247 248 mAccountTypeManager = AccountTypeManager.getInstance(getContext()); 249 mAccountDisplayInfoFactory = AccountDisplayInfoFactory.forWritableAccounts(getContext()); 250 mLayoutInflater = (LayoutInflater) 251 getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE); 252 253 // Account header 254 mAccountHeaderContainer = findViewById(R.id.account_header_container); 255 mAccountHeaderType = (TextView) findViewById(R.id.account_type); 256 mAccountHeaderName = (TextView) findViewById(R.id.account_name); 257 mAccountHeaderIcon = (ImageView) findViewById(R.id.account_type_icon); 258 mAccountHeaderExpanderIcon = (ImageView) findViewById(R.id.account_expander_icon); 259 260 mPhotoView = (PhotoEditorView) findViewById(R.id.photo_editor); 261 mKindSectionViews = (LinearLayout) findViewById(R.id.kind_section_views); 262 mMoreFields = findViewById(R.id.more_fields); 263 mMoreFields.setOnClickListener(this); 264 } 265 266 @Override 267 public void onClick(View view) { 268 if (view.getId() == R.id.more_fields) { 269 showAllFields(); 270 } 271 } 272 273 @Override 274 public void setEnabled(boolean enabled) { 275 super.setEnabled(enabled); 276 final int childCount = mKindSectionViews.getChildCount(); 277 for (int i = 0; i < childCount; i++) { 278 mKindSectionViews.getChildAt(i).setEnabled(enabled); 279 } 280 } 281 282 @Override 283 public Parcelable onSaveInstanceState() { 284 final Parcelable superState = super.onSaveInstanceState(); 285 final SavedState savedState = new SavedState(superState); 286 savedState.mIsExpanded = mIsExpanded; 287 return savedState; 288 } 289 290 @Override 291 public void onRestoreInstanceState(Parcelable state) { 292 if(!(state instanceof SavedState)) { 293 super.onRestoreInstanceState(state); 294 return; 295 } 296 final SavedState savedState = (SavedState) state; 297 super.onRestoreInstanceState(savedState.getSuperState()); 298 mIsExpanded = savedState.mIsExpanded; 299 if (mIsExpanded) { 300 showAllFields(); 301 } 302 } 303 304 /** 305 * Pass through to {@link PhotoEditorView#setListener}. 306 */ 307 public void setPhotoListener(PhotoEditorView.Listener listener) { 308 mPhotoView.setListener(listener); 309 } 310 311 public void removePhoto() { 312 mPhotoValuesDelta.setFromTemplate(true); 313 mPhotoValuesDelta.put(Photo.PHOTO, (byte[]) null); 314 mPhotoValuesDelta.put(Photo.PHOTO_FILE_ID, (String) null); 315 316 mPhotoView.removePhoto(); 317 } 318 319 /** 320 * Pass through to {@link PhotoEditorView#setFullSizedPhoto(Uri)}. 321 */ 322 public void setFullSizePhoto(Uri photoUri) { 323 mPhotoView.setFullSizedPhoto(photoUri); 324 } 325 326 public void updatePhoto(Uri photoUri) { 327 mPhotoValuesDelta.setFromTemplate(false); 328 // Unset primary for all photos 329 unsetSuperPrimaryFromAllPhotos(); 330 // Mark the currently displayed photo as primary 331 mPhotoValuesDelta.setSuperPrimary(true); 332 333 // Even though high-res photos cannot be saved by passing them via 334 // an EntityDeltaList (since they cause the Bundle size limit to be 335 // exceeded), we still pass a low-res thumbnail. This simplifies 336 // code all over the place, because we don't have to test whether 337 // there is a change in EITHER the delta-list OR a changed photo... 338 // this way, there is always a change in the delta-list. 339 try { 340 final byte[] bytes = EditorUiUtils.getCompressedThumbnailBitmapBytes( 341 getContext(), photoUri); 342 if (bytes != null) { 343 mPhotoValuesDelta.setPhoto(bytes); 344 } 345 } catch (FileNotFoundException e) { 346 elog("Failed to get bitmap from photo Uri"); 347 } 348 349 mPhotoView.setFullSizedPhoto(photoUri); 350 } 351 352 private void unsetSuperPrimaryFromAllPhotos() { 353 for (int i = 0; i < mRawContactDeltas.size(); i++) { 354 final RawContactDelta rawContactDelta = mRawContactDeltas.get(i); 355 if (!rawContactDelta.hasMimeEntries(Photo.CONTENT_ITEM_TYPE)) { 356 continue; 357 } 358 final List<ValuesDelta> photosDeltas = 359 mRawContactDeltas.get(i).getMimeEntries(Photo.CONTENT_ITEM_TYPE); 360 if (photosDeltas == null) { 361 continue; 362 } 363 for (int j = 0; j < photosDeltas.size(); j++) { 364 photosDeltas.get(j).setSuperPrimary(false); 365 } 366 } 367 } 368 369 /** 370 * Pass through to {@link PhotoEditorView#isWritablePhotoSet}. 371 */ 372 public boolean isWritablePhotoSet() { 373 return mPhotoView.isWritablePhotoSet(); 374 } 375 376 /** 377 * Get the raw contact ID for the current photo. 378 */ 379 public long getPhotoRawContactId() { 380 return mCurrentRawContactDelta.getRawContactId(); 381 } 382 383 public StructuredNameEditorView getNameEditorView() { 384 final KindSectionView nameKindSectionView = mKindSectionViewMap 385 .get(StructuredName.CONTENT_ITEM_TYPE); 386 return nameKindSectionView == null 387 ? null : nameKindSectionView.getNameEditorView(); 388 } 389 390 public RawContactDelta getCurrentRawContactDelta() { 391 return mCurrentRawContactDelta; 392 } 393 394 /** 395 * Marks the raw contact photo given as primary for the aggregate contact. 396 */ 397 public void setPrimaryPhoto() { 398 399 // Update values delta 400 final ValuesDelta valuesDelta = mCurrentRawContactDelta 401 .getSuperPrimaryEntry(Photo.CONTENT_ITEM_TYPE); 402 if (valuesDelta == null) { 403 Log.wtf(TAG, "setPrimaryPhoto: had no ValuesDelta for the current RawContactDelta"); 404 return; 405 } 406 valuesDelta.setFromTemplate(false); 407 unsetSuperPrimaryFromAllPhotos(); 408 valuesDelta.setSuperPrimary(true); 409 } 410 411 public View getAggregationAnchorView() { 412 final StructuredNameEditorView nameEditorView = getNameEditorView(); 413 return nameEditorView != null ? nameEditorView.findViewById(R.id.anchor_view) : null; 414 } 415 416 public void setGroupMetaData(Cursor groupMetaData) { 417 final KindSectionView groupKindSectionView = 418 mKindSectionViewMap.get(GroupMembership.CONTENT_ITEM_TYPE); 419 if (groupKindSectionView == null) { 420 return; 421 } 422 groupKindSectionView.setGroupMetaData(groupMetaData); 423 if (mIsExpanded) { 424 groupKindSectionView.setHideWhenEmpty(false); 425 groupKindSectionView.updateEmptyEditors(/* shouldAnimate =*/ true); 426 } 427 } 428 429 public void setState(RawContactDeltaList rawContactDeltas, 430 MaterialColorMapUtils.MaterialPalette materialPalette, ViewIdGenerator viewIdGenerator, 431 boolean hasNewContact, boolean isUserProfile, AccountWithDataSet primaryAccount, 432 long rawContactIdToDisplayAlone, boolean isEditingReadOnlyRawContactWithNewContact) { 433 434 mRawContactDeltas = rawContactDeltas; 435 mRawContactIdToDisplayAlone = rawContactIdToDisplayAlone; 436 mIsEditingReadOnlyRawContactWithNewContact = isEditingReadOnlyRawContactWithNewContact; 437 438 mKindSectionViewMap.clear(); 439 mKindSectionViews.removeAllViews(); 440 mMoreFields.setVisibility(View.VISIBLE); 441 442 mMaterialPalette = materialPalette; 443 mViewIdGenerator = viewIdGenerator; 444 445 mHasNewContact = hasNewContact; 446 mIsUserProfile = isUserProfile; 447 mPrimaryAccount = primaryAccount; 448 if (mPrimaryAccount == null) { 449 mPrimaryAccount = ContactEditorUtils.create(getContext()).getOnlyOrDefaultAccount(); 450 } 451 vlog("state: primary " + mPrimaryAccount); 452 453 // Parse the given raw contact deltas 454 if (rawContactDeltas == null || rawContactDeltas.isEmpty()) { 455 elog("No raw contact deltas"); 456 if (mListener != null) mListener.onBindEditorsFailed(); 457 return; 458 } 459 pickRawContactDelta(); 460 parseRawContactDelta(); 461 if (mKindSectionDataMap.isEmpty()) { 462 elog("No kind section data parsed from RawContactDelta(s)"); 463 if (mListener != null) mListener.onBindEditorsFailed(); 464 return; 465 } 466 467 final KindSectionData nameSectionData = 468 mKindSectionDataMap.get(StructuredName.CONTENT_ITEM_TYPE); 469 // Ensure that a structured name and photo exists 470 if (nameSectionData != null) { 471 final RawContactDelta rawContactDelta = 472 nameSectionData.getRawContactDelta(); 473 RawContactModifier.ensureKindExists( 474 rawContactDelta, 475 rawContactDelta.getAccountType(mAccountTypeManager), 476 StructuredName.CONTENT_ITEM_TYPE); 477 RawContactModifier.ensureKindExists( 478 rawContactDelta, 479 rawContactDelta.getAccountType(mAccountTypeManager), 480 Photo.CONTENT_ITEM_TYPE); 481 } 482 483 // Setup the view 484 addPhotoView(); 485 if (isReadOnlyRawContact()) { 486 // We're want to display the inputs fields for a single read only raw contact 487 addReadOnlyRawContactEditorViews(); 488 } else { 489 setupEditorNormally(); 490 } 491 if (mListener != null) mListener.onEditorsBound(); 492 } 493 494 private void setupEditorNormally() { 495 addAccountInfo(); 496 addKindSectionViews(); 497 498 mMoreFields.setVisibility(hasMoreFields() ? View.VISIBLE : View.GONE); 499 500 if (mIsExpanded) showAllFields(); 501 } 502 503 private boolean isReadOnlyRawContact() { 504 return !mCurrentRawContactDelta.getAccountType(mAccountTypeManager).areContactsWritable(); 505 } 506 507 private void pickRawContactDelta() { 508 vlog("parse: " + mRawContactDeltas.size() + " rawContactDelta(s)"); 509 for (int j = 0; j < mRawContactDeltas.size(); j++) { 510 final RawContactDelta rawContactDelta = mRawContactDeltas.get(j); 511 vlog("parse: " + j + " rawContactDelta" + rawContactDelta); 512 if (rawContactDelta == null || !rawContactDelta.isVisible()) continue; 513 final AccountType accountType = rawContactDelta.getAccountType(mAccountTypeManager); 514 if (accountType == null) continue; 515 516 if (mRawContactIdToDisplayAlone > 0) { 517 // Look for the raw contact if specified. 518 if (rawContactDelta.getRawContactId().equals(mRawContactIdToDisplayAlone)) { 519 mCurrentRawContactDelta = rawContactDelta; 520 return; 521 } 522 } else if (mPrimaryAccount != null 523 && mPrimaryAccount.equals(rawContactDelta.getAccountWithDataSet())) { 524 // Otherwise try to find the one that matches the default. 525 mCurrentRawContactDelta = rawContactDelta; 526 return; 527 } else if (accountType.areContactsWritable()){ 528 // TODO: Find better raw contact delta 529 // Just select an arbitrary writable contact. 530 mCurrentRawContactDelta = rawContactDelta; 531 } 532 } 533 534 } 535 536 private void parseRawContactDelta() { 537 mKindSectionDataMap.clear(); 538 mSortedMimetypes.clear(); 539 540 final AccountType accountType = mCurrentRawContactDelta.getAccountType(mAccountTypeManager); 541 final List<DataKind> dataKinds = accountType.getSortedDataKinds(); 542 final int dataKindSize = dataKinds == null ? 0 : dataKinds.size(); 543 vlog("parse: " + dataKindSize + " dataKinds(s)"); 544 545 for (int i = 0; i < dataKindSize; i++) { 546 final DataKind dataKind = dataKinds.get(i); 547 // Skip null and un-editable fields. 548 if (dataKind == null || !dataKind.editable) { 549 vlog("parse: " + i + 550 (dataKind == null ? " dropped null data kind" 551 : " dropped uneditable mimetype: " + dataKind.mimeType)); 552 continue; 553 } 554 final String mimeType = dataKind.mimeType; 555 556 // Skip psuedo mime types 557 if (DataKind.PSEUDO_MIME_TYPE_NAME.equals(mimeType) || 558 DataKind.PSEUDO_MIME_TYPE_PHONETIC_NAME.equals(mimeType)) { 559 vlog("parse: " + i + " " + dataKind.mimeType + " dropped pseudo type"); 560 continue; 561 } 562 563 // Skip custom fields 564 // TODO: Handle them when we implement editing custom fields. 565 if (CustomDataItem.MIMETYPE_CUSTOM_FIELD.equals(mimeType)) { 566 vlog("parse: " + i + " " + dataKind.mimeType + " dropped custom field"); 567 continue; 568 } 569 570 final KindSectionData kindSectionData = 571 new KindSectionData(accountType, dataKind, mCurrentRawContactDelta); 572 mKindSectionDataMap.put(mimeType, kindSectionData); 573 mSortedMimetypes.add(mimeType); 574 575 vlog("parse: " + i + " " + dataKind.mimeType + " " + 576 kindSectionData.getValuesDeltas().size() + " value(s) " + 577 kindSectionData.getNonEmptyValuesDeltas().size() + " non-empty value(s) " + 578 kindSectionData.getVisibleValuesDeltas().size() + 579 " visible value(s)"); 580 } 581 } 582 583 private void addReadOnlyRawContactEditorViews() { 584 mKindSectionViews.removeAllViews(); 585 addAccountInfo(); 586 final AccountTypeManager accountTypes = AccountTypeManager.getInstance( 587 getContext()); 588 final AccountType type = mCurrentRawContactDelta.getAccountType(accountTypes); 589 590 // Bail if invalid state or source 591 if (type == null) return; 592 593 // Make sure we have StructuredName 594 RawContactModifier.ensureKindExists( 595 mCurrentRawContactDelta, type, StructuredName.CONTENT_ITEM_TYPE); 596 597 ValuesDelta primary; 598 599 // Name 600 final Context context = getContext(); 601 final Resources res = context.getResources(); 602 primary = mCurrentRawContactDelta.getPrimaryEntry(StructuredName.CONTENT_ITEM_TYPE); 603 final String name = primary != null ? primary.getAsString(StructuredName.DISPLAY_NAME) : 604 getContext().getString(R.string.missing_name); 605 final Drawable nameDrawable = context.getDrawable(R.drawable.ic_person_24dp); 606 final String nameContentDescription = res.getString(R.string.header_name_entry); 607 bindData(nameDrawable, nameContentDescription, name, /* type */ null, 608 /* isFirstEntry */ true); 609 610 // Phones 611 final ArrayList<ValuesDelta> phones = mCurrentRawContactDelta 612 .getMimeEntries(Phone.CONTENT_ITEM_TYPE); 613 final Drawable phoneDrawable = context.getDrawable(R.drawable.ic_phone_24dp); 614 final String phoneContentDescription = res.getString(R.string.header_phone_entry); 615 if (phones != null) { 616 boolean isFirstPhoneBound = true; 617 for (ValuesDelta phone : phones) { 618 final String phoneNumber = phone.getPhoneNumber(); 619 if (TextUtils.isEmpty(phoneNumber)) { 620 continue; 621 } 622 final String formattedNumber = PhoneNumberUtilsCompat.formatNumber( 623 phoneNumber, phone.getPhoneNormalizedNumber(), 624 GeoUtil.getCurrentCountryIso(getContext())); 625 CharSequence phoneType = null; 626 if (phone.hasPhoneType()) { 627 phoneType = Phone.getTypeLabel( 628 res, phone.getPhoneType(), phone.getPhoneLabel()); 629 } 630 bindData(phoneDrawable, phoneContentDescription, formattedNumber, phoneType, 631 isFirstPhoneBound, true); 632 isFirstPhoneBound = false; 633 } 634 } 635 636 // Emails 637 final ArrayList<ValuesDelta> emails = mCurrentRawContactDelta 638 .getMimeEntries(Email.CONTENT_ITEM_TYPE); 639 final Drawable emailDrawable = context.getDrawable(R.drawable.ic_email_24dp); 640 final String emailContentDescription = res.getString(R.string.header_email_entry); 641 if (emails != null) { 642 boolean isFirstEmailBound = true; 643 for (ValuesDelta email : emails) { 644 final String emailAddress = email.getEmailData(); 645 if (TextUtils.isEmpty(emailAddress)) { 646 continue; 647 } 648 CharSequence emailType = null; 649 if (email.hasEmailType()) { 650 emailType = Email.getTypeLabel( 651 res, email.getEmailType(), email.getEmailLabel()); 652 } 653 bindData(emailDrawable, emailContentDescription, emailAddress, emailType, 654 isFirstEmailBound); 655 isFirstEmailBound = false; 656 } 657 } 658 659 mKindSectionViews.setVisibility(mKindSectionViews.getChildCount() > 0 ? VISIBLE : GONE); 660 // Hide the "More fields" link 661 mMoreFields.setVisibility(GONE); 662 } 663 664 private void bindData(Drawable icon, String iconContentDescription, CharSequence data, 665 CharSequence type, boolean isFirstEntry) { 666 bindData(icon, iconContentDescription, data, type, isFirstEntry, false); 667 } 668 669 private void bindData(Drawable icon, String iconContentDescription, CharSequence data, 670 CharSequence type, boolean isFirstEntry, boolean forceLTR) { 671 final LayoutInflater inflater = (LayoutInflater) getContext().getSystemService( 672 Context.LAYOUT_INFLATER_SERVICE); 673 final View field = inflater.inflate(R.layout.item_read_only_field, mKindSectionViews, 674 false); 675 if (isFirstEntry) { 676 final ImageView imageView = (ImageView) field.findViewById(R.id.kind_icon); 677 imageView.setImageDrawable(icon); 678 imageView.setContentDescription(iconContentDescription); 679 } else { 680 final ImageView imageView = (ImageView) field.findViewById(R.id.kind_icon); 681 imageView.setVisibility(View.INVISIBLE); 682 imageView.setContentDescription(null); 683 } 684 final TextView dataView = (TextView) field.findViewById(R.id.data); 685 dataView.setText(data); 686 if (forceLTR) { 687 dataView.setTextDirection(View.TEXT_DIRECTION_LTR); 688 } 689 final TextView typeView = (TextView) field.findViewById(R.id.type); 690 if (!TextUtils.isEmpty(type)) { 691 typeView.setText(type); 692 } else { 693 typeView.setVisibility(View.GONE); 694 } 695 mKindSectionViews.addView(field); 696 } 697 698 private void addAccountInfo() { 699 mAccountHeaderContainer.setVisibility(View.GONE); 700 701 final AccountDisplayInfo account = 702 mAccountDisplayInfoFactory.getAccountDisplayInfoFor(mCurrentRawContactDelta); 703 704 // Get the account information for the primary raw contact delta 705 final String accountLabel = mIsUserProfile 706 ? EditorUiUtils.getAccountHeaderLabelForMyProfile(getContext(), account) 707 : account.getNameLabel().toString(); 708 709 addAccountHeader(accountLabel); 710 711 // If we're saving a new contact and there are multiple accounts, add the account selector. 712 final List<AccountWithDataSet> accounts = 713 AccountTypeManager.getInstance(getContext()).getAccounts(true); 714 if (mHasNewContact && !mIsUserProfile && accounts.size() > 1) { 715 addAccountSelector(mCurrentRawContactDelta); 716 } 717 } 718 719 private void addAccountHeader(String accountLabel) { 720 mAccountHeaderContainer.setVisibility(View.VISIBLE); 721 722 // Set the account name 723 mAccountHeaderName.setVisibility(View.VISIBLE); 724 mAccountHeaderName.setText(accountLabel); 725 726 // Set the account type 727 final String selectorTitle = getResources().getString(isReadOnlyRawContact() ? 728 R.string.editor_account_selector_read_only_title : 729 R.string.editor_account_selector_title); 730 mAccountHeaderType.setText(selectorTitle); 731 732 // Set the icon 733 final AccountType accountType = 734 mCurrentRawContactDelta.getRawContactAccountType(getContext()); 735 mAccountHeaderIcon.setImageDrawable(accountType.getDisplayIcon(getContext())); 736 737 // Set the content description 738 mAccountHeaderContainer.setContentDescription( 739 EditorUiUtils.getAccountInfoContentDescription(accountLabel, 740 selectorTitle)); 741 } 742 743 private void addAccountSelector(final RawContactDelta rawContactDelta) { 744 // Add handlers for choosing another account to save to. 745 mAccountHeaderExpanderIcon.setVisibility(View.VISIBLE); 746 mAccountHeaderContainer.setOnClickListener(new View.OnClickListener() { 747 @Override 748 public void onClick(View v) { 749 final ListPopupWindow popup = new ListPopupWindow(getContext(), null); 750 final AccountsListAdapter adapter = 751 new AccountsListAdapter(getContext(), 752 AccountsListAdapter.AccountListFilter.ACCOUNTS_CONTACT_WRITABLE, 753 mPrimaryAccount); 754 popup.setWidth(mAccountHeaderContainer.getWidth()); 755 popup.setAnchorView(mAccountHeaderContainer); 756 popup.setAdapter(adapter); 757 popup.setModal(true); 758 popup.setInputMethodMode(ListPopupWindow.INPUT_METHOD_NOT_NEEDED); 759 popup.setOnItemClickListener(new AdapterView.OnItemClickListener() { 760 @Override 761 public void onItemClick(AdapterView<?> parent, View view, int position, 762 long id) { 763 UiClosables.closeQuietly(popup); 764 final AccountWithDataSet newAccount = adapter.getItem(position); 765 if (mListener != null && !mPrimaryAccount.equals(newAccount)) { 766 mIsExpanded = false; 767 mListener.onRebindEditorsForNewContact( 768 rawContactDelta, 769 mPrimaryAccount, 770 newAccount); 771 } 772 } 773 }); 774 popup.show(); 775 } 776 }); 777 } 778 779 private void addPhotoView() { 780 if (!mCurrentRawContactDelta.hasMimeEntries(Photo.CONTENT_ITEM_TYPE)) { 781 wlog("No photo mimetype for this raw contact."); 782 mPhotoView.setVisibility(GONE); 783 return; 784 } else { 785 mPhotoView.setVisibility(VISIBLE); 786 } 787 788 final ValuesDelta superPrimaryDelta = mCurrentRawContactDelta 789 .getSuperPrimaryEntry(Photo.CONTENT_ITEM_TYPE); 790 if (superPrimaryDelta == null) { 791 Log.wtf(TAG, "addPhotoView: no ValueDelta found for current RawContactDelta" 792 + "that supports a photo."); 793 mPhotoView.setVisibility(GONE); 794 return; 795 } 796 // Set the photo view 797 mPhotoView.setPalette(mMaterialPalette); 798 mPhotoView.setPhoto(superPrimaryDelta); 799 800 if (isReadOnlyRawContact()) { 801 mPhotoView.setReadOnly(true); 802 return; 803 } 804 mPhotoView.setReadOnly(false); 805 mPhotoValuesDelta = superPrimaryDelta; 806 } 807 808 private void addKindSectionViews() { 809 int i = -1; 810 811 for (String mimeType : mSortedMimetypes) { 812 i++; 813 // Ignore mime types that we've already handled 814 if (Photo.CONTENT_ITEM_TYPE.equals(mimeType)) { 815 vlog("kind: " + i + " " + mimeType + " dropped"); 816 continue; 817 } 818 final KindSectionView kindSectionView; 819 final KindSectionData kindSectionData = mKindSectionDataMap.get(mimeType); 820 kindSectionView = inflateKindSectionView(mKindSectionViews, kindSectionData, mimeType); 821 mKindSectionViews.addView(kindSectionView); 822 823 // Keep a pointer to the KindSectionView for each mimeType 824 mKindSectionViewMap.put(mimeType, kindSectionView); 825 } 826 } 827 828 private KindSectionView inflateKindSectionView(ViewGroup viewGroup, 829 KindSectionData kindSectionData, String mimeType) { 830 final KindSectionView kindSectionView = (KindSectionView) 831 mLayoutInflater.inflate(R.layout.item_kind_section, viewGroup, 832 /* attachToRoot =*/ false); 833 kindSectionView.setIsUserProfile(mIsUserProfile); 834 835 if (Phone.CONTENT_ITEM_TYPE.equals(mimeType) 836 || Email.CONTENT_ITEM_TYPE.equals(mimeType)) { 837 // Phone numbers and email addresses are always displayed, 838 // even if they are empty 839 kindSectionView.setHideWhenEmpty(false); 840 } 841 842 // Since phone numbers and email addresses displayed even if they are empty, 843 // they will be the only types you add new values to initially for new contacts 844 kindSectionView.setShowOneEmptyEditor(true); 845 846 kindSectionView.setState(kindSectionData, mViewIdGenerator, mListener); 847 848 return kindSectionView; 849 } 850 851 private void showAllFields() { 852 // Stop hiding empty editors and allow the user to enter values for all kinds now 853 for (int i = 0; i < mKindSectionViews.getChildCount(); i++) { 854 final KindSectionView kindSectionView = 855 (KindSectionView) mKindSectionViews.getChildAt(i); 856 kindSectionView.setHideWhenEmpty(false); 857 kindSectionView.updateEmptyEditors(/* shouldAnimate =*/ true); 858 } 859 mIsExpanded = true; 860 861 // Hide the more fields button 862 mMoreFields.setVisibility(View.GONE); 863 } 864 865 private boolean hasMoreFields() { 866 for (KindSectionView section : mKindSectionViewMap.values()) { 867 if (section.getVisibility() != View.VISIBLE) { 868 return true; 869 } 870 } 871 return false; 872 } 873 874 private static void vlog(String message) { 875 if (Log.isLoggable(TAG, Log.VERBOSE)) { 876 Log.v(TAG, message); 877 } 878 } 879 880 private static void wlog(String message) { 881 if (Log.isLoggable(TAG, Log.WARN)) { 882 Log.w(TAG, message); 883 } 884 } 885 886 private static void elog(String message) { 887 Log.e(TAG, message); 888 } 889} 890