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