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.AccountInfo; 62import com.android.contacts.model.account.AccountType; 63import com.android.contacts.model.account.AccountWithDataSet; 64import com.android.contacts.model.dataitem.CustomDataItem; 65import com.android.contacts.model.dataitem.DataKind; 66import com.android.contacts.util.AccountsListAdapter; 67import com.android.contacts.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 LayoutInflater mLayoutInflater; 199 200 private ViewIdGenerator mViewIdGenerator; 201 private MaterialColorMapUtils.MaterialPalette mMaterialPalette; 202 private boolean mHasNewContact; 203 private boolean mIsUserProfile; 204 private AccountWithDataSet mPrimaryAccount; 205 private List<AccountInfo> mAccounts = new ArrayList<>(); 206 private RawContactDeltaList mRawContactDeltas; 207 private RawContactDelta mCurrentRawContactDelta; 208 private long mRawContactIdToDisplayAlone = -1; 209 private Map<String, KindSectionData> mKindSectionDataMap = new HashMap<>(); 210 private Set<String> mSortedMimetypes = new TreeSet<>(new MimeTypeComparator()); 211 212 // Account header 213 private View mAccountHeaderContainer; 214 private TextView mAccountHeaderPrimaryText; 215 private TextView mAccountHeaderSecondaryText; 216 private ImageView mAccountHeaderIcon; 217 private ImageView mAccountHeaderExpanderIcon; 218 219 private PhotoEditorView mPhotoView; 220 private ViewGroup mKindSectionViews; 221 private Map<String, KindSectionView> mKindSectionViewMap = new HashMap<>(); 222 private View mMoreFields; 223 224 private boolean mIsExpanded; 225 226 private Bundle mIntentExtras; 227 228 private ValuesDelta mPhotoValuesDelta; 229 230 public RawContactEditorView(Context context) { 231 super(context); 232 } 233 234 public RawContactEditorView(Context context, AttributeSet attrs) { 235 super(context, attrs); 236 } 237 238 /** 239 * Sets the receiver for {@link RawContactEditorView} callbacks. 240 */ 241 public void setListener(Listener listener) { 242 mListener = listener; 243 } 244 245 @Override 246 protected void onFinishInflate() { 247 super.onFinishInflate(); 248 249 mAccountTypeManager = AccountTypeManager.getInstance(getContext()); 250 mLayoutInflater = (LayoutInflater) 251 getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE); 252 253 // Account header 254 mAccountHeaderContainer = findViewById(R.id.account_header_container); 255 mAccountHeaderPrimaryText = (TextView) findViewById(R.id.account_type); 256 mAccountHeaderSecondaryText = (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 == null ? - 1 : 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 setIntentExtras(Bundle extras) { 430 mIntentExtras = extras; 431 } 432 433 public void setState(RawContactDeltaList rawContactDeltas, 434 MaterialColorMapUtils.MaterialPalette materialPalette, ViewIdGenerator viewIdGenerator, 435 boolean hasNewContact, boolean isUserProfile, AccountWithDataSet primaryAccount, 436 long rawContactIdToDisplayAlone) { 437 438 mRawContactDeltas = rawContactDeltas; 439 mRawContactIdToDisplayAlone = rawContactIdToDisplayAlone; 440 441 mKindSectionViewMap.clear(); 442 mKindSectionViews.removeAllViews(); 443 mMoreFields.setVisibility(View.VISIBLE); 444 445 mMaterialPalette = materialPalette; 446 mViewIdGenerator = viewIdGenerator; 447 448 mHasNewContact = hasNewContact; 449 mIsUserProfile = isUserProfile; 450 mPrimaryAccount = primaryAccount; 451 if (mPrimaryAccount == null && mAccounts != null) { 452 mPrimaryAccount = ContactEditorUtils.create(getContext()) 453 .getOnlyOrDefaultAccount(AccountInfo.extractAccounts(mAccounts)); 454 } 455 if (Log.isLoggable(TAG, Log.VERBOSE)) { 456 Log.v(TAG, "state: primary " + mPrimaryAccount); 457 } 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 if (mCurrentRawContactDelta == null) { 467 elog("Couldn't pick a raw contact delta."); 468 if (mListener != null) mListener.onBindEditorsFailed(); 469 return; 470 } 471 // Apply any intent extras now that we have selected a raw contact delta. 472 applyIntentExtras(); 473 parseRawContactDelta(); 474 if (mKindSectionDataMap.isEmpty()) { 475 elog("No kind section data parsed from RawContactDelta(s)"); 476 if (mListener != null) mListener.onBindEditorsFailed(); 477 return; 478 } 479 480 final KindSectionData nameSectionData = 481 mKindSectionDataMap.get(StructuredName.CONTENT_ITEM_TYPE); 482 // Ensure that a structured name and photo exists 483 if (nameSectionData != null) { 484 final RawContactDelta rawContactDelta = 485 nameSectionData.getRawContactDelta(); 486 RawContactModifier.ensureKindExists( 487 rawContactDelta, 488 rawContactDelta.getAccountType(mAccountTypeManager), 489 StructuredName.CONTENT_ITEM_TYPE); 490 RawContactModifier.ensureKindExists( 491 rawContactDelta, 492 rawContactDelta.getAccountType(mAccountTypeManager), 493 Photo.CONTENT_ITEM_TYPE); 494 } 495 496 // Setup the view 497 addPhotoView(); 498 setAccountInfo(); 499 if (isReadOnlyRawContact()) { 500 // We're want to display the inputs fields for a single read only raw contact 501 addReadOnlyRawContactEditorViews(); 502 } else { 503 setupEditorNormally(); 504 // If we're inserting a new contact, request focus to bring up the keyboard for the 505 // name field. 506 if (mHasNewContact) { 507 final StructuredNameEditorView name = getNameEditorView(); 508 if (name != null) { 509 name.requestFocusForFirstEditField(); 510 } 511 } 512 } 513 if (mListener != null) mListener.onEditorsBound(); 514 } 515 516 public void setAccounts(List<AccountInfo> accounts) { 517 mAccounts.clear(); 518 mAccounts.addAll(accounts); 519 // Update the account header 520 setAccountInfo(); 521 } 522 523 private void setupEditorNormally() { 524 addKindSectionViews(); 525 526 mMoreFields.setVisibility(hasMoreFields() ? View.VISIBLE : View.GONE); 527 528 if (mIsExpanded) showAllFields(); 529 } 530 531 private boolean isReadOnlyRawContact() { 532 return !mCurrentRawContactDelta.getAccountType(mAccountTypeManager).areContactsWritable(); 533 } 534 535 private void pickRawContactDelta() { 536 if (Log.isLoggable(TAG, Log.VERBOSE)) { 537 Log.v(TAG, "parse: " + mRawContactDeltas.size() + " rawContactDelta(s)"); 538 } 539 for (int j = 0; j < mRawContactDeltas.size(); j++) { 540 final RawContactDelta rawContactDelta = mRawContactDeltas.get(j); 541 if (Log.isLoggable(TAG, Log.VERBOSE)) { 542 Log.v(TAG, "parse: " + j + " rawContactDelta" + rawContactDelta); 543 } 544 if (rawContactDelta == null || !rawContactDelta.isVisible()) continue; 545 final AccountType accountType = rawContactDelta.getAccountType(mAccountTypeManager); 546 if (accountType == null) continue; 547 548 if (mRawContactIdToDisplayAlone > 0) { 549 // Look for the raw contact if specified. 550 if (rawContactDelta.getRawContactId().equals(mRawContactIdToDisplayAlone)) { 551 mCurrentRawContactDelta = rawContactDelta; 552 return; 553 } 554 } else if (mPrimaryAccount != null 555 && mPrimaryAccount.equals(rawContactDelta.getAccountWithDataSet())) { 556 // Otherwise try to find the one that matches the default. 557 mCurrentRawContactDelta = rawContactDelta; 558 return; 559 } else if (accountType.areContactsWritable()){ 560 // TODO: Find better raw contact delta 561 // Just select an arbitrary writable contact. 562 mCurrentRawContactDelta = rawContactDelta; 563 } 564 } 565 566 } 567 568 private void applyIntentExtras() { 569 if (mIntentExtras == null || mIntentExtras.size() == 0) { 570 return; 571 } 572 final AccountTypeManager accountTypes = AccountTypeManager.getInstance(getContext()); 573 final AccountType type = mCurrentRawContactDelta.getAccountType(accountTypes); 574 575 RawContactModifier.parseExtras(getContext(), type, mCurrentRawContactDelta, mIntentExtras); 576 mIntentExtras = null; 577 } 578 579 private void parseRawContactDelta() { 580 mKindSectionDataMap.clear(); 581 mSortedMimetypes.clear(); 582 583 final AccountType accountType = mCurrentRawContactDelta.getAccountType(mAccountTypeManager); 584 final List<DataKind> dataKinds = accountType.getSortedDataKinds(); 585 final int dataKindSize = dataKinds == null ? 0 : dataKinds.size(); 586 if (Log.isLoggable(TAG, Log.VERBOSE)) { 587 Log.v(TAG, "parse: " + dataKindSize + " dataKinds(s)"); 588 } 589 590 for (int i = 0; i < dataKindSize; i++) { 591 final DataKind dataKind = dataKinds.get(i); 592 // Skip null and un-editable fields. 593 if (dataKind == null || !dataKind.editable) { 594 if (Log.isLoggable(TAG, Log.VERBOSE)) { 595 Log.v(TAG, "parse: " + i + 596 (dataKind == null ? " dropped null data kind" 597 : " dropped uneditable mimetype: " + dataKind.mimeType)); 598 } 599 continue; 600 } 601 final String mimeType = dataKind.mimeType; 602 603 // Skip psuedo mime types 604 if (DataKind.PSEUDO_MIME_TYPE_NAME.equals(mimeType) || 605 DataKind.PSEUDO_MIME_TYPE_PHONETIC_NAME.equals(mimeType)) { 606 if (Log.isLoggable(TAG, Log.VERBOSE)) { 607 Log.v(TAG, "parse: " + i + " " + dataKind.mimeType + " dropped pseudo type"); 608 } 609 continue; 610 } 611 612 // Skip custom fields 613 // TODO: Handle them when we implement editing custom fields. 614 if (CustomDataItem.MIMETYPE_CUSTOM_FIELD.equals(mimeType)) { 615 if (Log.isLoggable(TAG, Log.VERBOSE)) { 616 Log.v(TAG, "parse: " + i + " " + dataKind.mimeType + " dropped custom field"); 617 } 618 continue; 619 } 620 621 final KindSectionData kindSectionData = 622 new KindSectionData(accountType, dataKind, mCurrentRawContactDelta); 623 mKindSectionDataMap.put(mimeType, kindSectionData); 624 mSortedMimetypes.add(mimeType); 625 626 if (Log.isLoggable(TAG, Log.VERBOSE)) { 627 Log.v(TAG, "parse: " + i + " " + dataKind.mimeType + " " + 628 kindSectionData.getValuesDeltas().size() + " value(s) " + 629 kindSectionData.getNonEmptyValuesDeltas().size() + " non-empty value(s) " + 630 kindSectionData.getVisibleValuesDeltas().size() + 631 " visible value(s)"); 632 } 633 } 634 } 635 636 private void addReadOnlyRawContactEditorViews() { 637 mKindSectionViews.removeAllViews(); 638 final AccountTypeManager accountTypes = AccountTypeManager.getInstance( 639 getContext()); 640 final AccountType type = mCurrentRawContactDelta.getAccountType(accountTypes); 641 642 // Bail if invalid state or source 643 if (type == null) return; 644 645 // Make sure we have StructuredName 646 RawContactModifier.ensureKindExists( 647 mCurrentRawContactDelta, type, StructuredName.CONTENT_ITEM_TYPE); 648 649 ValuesDelta primary; 650 651 // Name 652 final Context context = getContext(); 653 final Resources res = context.getResources(); 654 primary = mCurrentRawContactDelta.getPrimaryEntry(StructuredName.CONTENT_ITEM_TYPE); 655 final String name = primary != null ? primary.getAsString(StructuredName.DISPLAY_NAME) : 656 getContext().getString(R.string.missing_name); 657 final Drawable nameDrawable = context.getDrawable(R.drawable.quantum_ic_person_vd_theme_24); 658 final String nameContentDescription = res.getString(R.string.header_name_entry); 659 bindData(nameDrawable, nameContentDescription, name, /* type */ null, 660 /* isFirstEntry */ true); 661 662 // Phones 663 final ArrayList<ValuesDelta> phones = mCurrentRawContactDelta 664 .getMimeEntries(Phone.CONTENT_ITEM_TYPE); 665 final Drawable phoneDrawable = context.getDrawable(R.drawable.quantum_ic_phone_vd_theme_24); 666 final String phoneContentDescription = res.getString(R.string.header_phone_entry); 667 if (phones != null) { 668 boolean isFirstPhoneBound = true; 669 for (ValuesDelta phone : phones) { 670 final String phoneNumber = phone.getPhoneNumber(); 671 if (TextUtils.isEmpty(phoneNumber)) { 672 continue; 673 } 674 final String formattedNumber = PhoneNumberUtilsCompat.formatNumber( 675 phoneNumber, phone.getPhoneNormalizedNumber(), 676 GeoUtil.getCurrentCountryIso(getContext())); 677 CharSequence phoneType = null; 678 if (phone.hasPhoneType()) { 679 phoneType = Phone.getTypeLabel( 680 res, phone.getPhoneType(), phone.getPhoneLabel()); 681 } 682 bindData(phoneDrawable, phoneContentDescription, formattedNumber, phoneType, 683 isFirstPhoneBound, true); 684 isFirstPhoneBound = false; 685 } 686 } 687 688 // Emails 689 final ArrayList<ValuesDelta> emails = mCurrentRawContactDelta 690 .getMimeEntries(Email.CONTENT_ITEM_TYPE); 691 final Drawable emailDrawable = context.getDrawable(R.drawable.quantum_ic_email_vd_theme_24); 692 final String emailContentDescription = res.getString(R.string.header_email_entry); 693 if (emails != null) { 694 boolean isFirstEmailBound = true; 695 for (ValuesDelta email : emails) { 696 final String emailAddress = email.getEmailData(); 697 if (TextUtils.isEmpty(emailAddress)) { 698 continue; 699 } 700 CharSequence emailType = null; 701 if (email.hasEmailType()) { 702 emailType = Email.getTypeLabel( 703 res, email.getEmailType(), email.getEmailLabel()); 704 } 705 bindData(emailDrawable, emailContentDescription, emailAddress, emailType, 706 isFirstEmailBound); 707 isFirstEmailBound = false; 708 } 709 } 710 711 mKindSectionViews.setVisibility(mKindSectionViews.getChildCount() > 0 ? VISIBLE : GONE); 712 // Hide the "More fields" link 713 mMoreFields.setVisibility(GONE); 714 } 715 716 private void bindData(Drawable icon, String iconContentDescription, CharSequence data, 717 CharSequence type, boolean isFirstEntry) { 718 bindData(icon, iconContentDescription, data, type, isFirstEntry, false); 719 } 720 721 private void bindData(Drawable icon, String iconContentDescription, CharSequence data, 722 CharSequence type, boolean isFirstEntry, boolean forceLTR) { 723 final View field = mLayoutInflater.inflate(R.layout.item_read_only_field, mKindSectionViews, 724 /* attachToRoot */ false); 725 if (isFirstEntry) { 726 final ImageView imageView = (ImageView) field.findViewById(R.id.kind_icon); 727 imageView.setImageDrawable(icon); 728 imageView.setContentDescription(iconContentDescription); 729 } else { 730 final ImageView imageView = (ImageView) field.findViewById(R.id.kind_icon); 731 imageView.setVisibility(View.INVISIBLE); 732 imageView.setContentDescription(null); 733 } 734 final TextView dataView = (TextView) field.findViewById(R.id.data); 735 dataView.setText(data); 736 if (forceLTR) { 737 dataView.setTextDirection(View.TEXT_DIRECTION_LTR); 738 } 739 final TextView typeView = (TextView) field.findViewById(R.id.type); 740 if (!TextUtils.isEmpty(type)) { 741 typeView.setText(type); 742 } else { 743 typeView.setVisibility(View.GONE); 744 } 745 mKindSectionViews.addView(field); 746 } 747 748 private void setAccountInfo() { 749 if (mCurrentRawContactDelta == null && mPrimaryAccount == null) { 750 return; 751 } 752 final AccountTypeManager accountTypeManager = AccountTypeManager.getInstance(getContext()); 753 final AccountInfo account = mCurrentRawContactDelta != null 754 ? accountTypeManager.getAccountInfoForAccount( 755 mCurrentRawContactDelta.getAccountWithDataSet()) 756 : accountTypeManager.getAccountInfoForAccount(mPrimaryAccount); 757 758 // Accounts haven't loaded yet or we are editing. 759 if (mAccounts.isEmpty()) { 760 mAccounts.add(account); 761 } 762 763 // Get the account information for the primary raw contact delta 764 if (isReadOnlyRawContact()) { 765 final String accountType = account.getTypeLabel().toString(); 766 setAccountHeader(accountType, 767 getResources().getString( 768 R.string.editor_account_selector_read_only_title, accountType)); 769 } else { 770 final String accountLabel = mIsUserProfile 771 ? EditorUiUtils.getAccountHeaderLabelForMyProfile(getContext(), account) 772 : account.getNameLabel().toString(); 773 setAccountHeader(getResources().getString(R.string.editor_account_selector_title), 774 accountLabel); 775 } 776 777 // If we're saving a new contact and there are multiple accounts, add the account selector. 778 if (mHasNewContact && !mIsUserProfile && mAccounts.size() > 1) { 779 addAccountSelector(mCurrentRawContactDelta); 780 } 781 } 782 783 private void setAccountHeader(String primaryText, String secondaryText) { 784 mAccountHeaderPrimaryText.setText(primaryText); 785 mAccountHeaderSecondaryText.setText(secondaryText); 786 787 // Set the icon 788 final AccountType accountType = 789 mCurrentRawContactDelta.getRawContactAccountType(getContext()); 790 mAccountHeaderIcon.setImageDrawable(accountType.getDisplayIcon(getContext())); 791 792 // Set the content description 793 mAccountHeaderContainer.setContentDescription( 794 EditorUiUtils.getAccountInfoContentDescription(secondaryText, primaryText)); 795 } 796 797 private void addAccountSelector(final RawContactDelta rawContactDelta) { 798 // Add handlers for choosing another account to save to. 799 mAccountHeaderExpanderIcon.setVisibility(View.VISIBLE); 800 final OnClickListener clickListener = new OnClickListener() { 801 @Override 802 public void onClick(View v) { 803 final AccountWithDataSet current = rawContactDelta.getAccountWithDataSet(); 804 AccountInfo.sortAccounts(current, mAccounts); 805 final ListPopupWindow popup = new ListPopupWindow(getContext(), null); 806 final AccountsListAdapter adapter = 807 new AccountsListAdapter(getContext(), mAccounts, current); 808 popup.setWidth(mAccountHeaderContainer.getWidth()); 809 popup.setAnchorView(mAccountHeaderContainer); 810 popup.setAdapter(adapter); 811 popup.setModal(true); 812 popup.setInputMethodMode(ListPopupWindow.INPUT_METHOD_NOT_NEEDED); 813 popup.setOnItemClickListener(new AdapterView.OnItemClickListener() { 814 @Override 815 public void onItemClick(AdapterView<?> parent, View view, int position, 816 long id) { 817 UiClosables.closeQuietly(popup); 818 final AccountWithDataSet newAccount = adapter.getItem(position); 819 if (mListener != null && !mPrimaryAccount.equals(newAccount)) { 820 mIsExpanded = false; 821 mListener.onRebindEditorsForNewContact( 822 rawContactDelta, 823 mPrimaryAccount, 824 newAccount); 825 } 826 } 827 }); 828 popup.show(); 829 } 830 }; 831 mAccountHeaderContainer.setOnClickListener(clickListener); 832 // Make the expander icon clickable so that it will be announced as a button by 833 // talkback 834 mAccountHeaderExpanderIcon.setOnClickListener(clickListener); 835 } 836 837 private void addPhotoView() { 838 if (!mCurrentRawContactDelta.hasMimeEntries(Photo.CONTENT_ITEM_TYPE)) { 839 wlog("No photo mimetype for this raw contact."); 840 mPhotoView.setVisibility(GONE); 841 return; 842 } else { 843 mPhotoView.setVisibility(VISIBLE); 844 } 845 846 final ValuesDelta superPrimaryDelta = mCurrentRawContactDelta 847 .getSuperPrimaryEntry(Photo.CONTENT_ITEM_TYPE); 848 if (superPrimaryDelta == null) { 849 Log.wtf(TAG, "addPhotoView: no ValueDelta found for current RawContactDelta" 850 + "that supports a photo."); 851 mPhotoView.setVisibility(GONE); 852 return; 853 } 854 // Set the photo view 855 mPhotoView.setPalette(mMaterialPalette); 856 mPhotoView.setPhoto(superPrimaryDelta); 857 858 if (isReadOnlyRawContact()) { 859 mPhotoView.setReadOnly(true); 860 return; 861 } 862 mPhotoView.setReadOnly(false); 863 mPhotoValuesDelta = superPrimaryDelta; 864 } 865 866 private void addKindSectionViews() { 867 int i = -1; 868 869 for (String mimeType : mSortedMimetypes) { 870 i++; 871 // Ignore mime types that we've already handled 872 if (Photo.CONTENT_ITEM_TYPE.equals(mimeType)) { 873 if (Log.isLoggable(TAG, Log.VERBOSE)) { 874 Log.v(TAG, "kind: " + i + " " + mimeType + " dropped"); 875 } 876 continue; 877 } 878 final KindSectionView kindSectionView; 879 final KindSectionData kindSectionData = mKindSectionDataMap.get(mimeType); 880 kindSectionView = inflateKindSectionView(mKindSectionViews, kindSectionData, mimeType); 881 mKindSectionViews.addView(kindSectionView); 882 883 // Keep a pointer to the KindSectionView for each mimeType 884 mKindSectionViewMap.put(mimeType, kindSectionView); 885 } 886 } 887 888 private KindSectionView inflateKindSectionView(ViewGroup viewGroup, 889 KindSectionData kindSectionData, String mimeType) { 890 final KindSectionView kindSectionView = (KindSectionView) 891 mLayoutInflater.inflate(R.layout.item_kind_section, viewGroup, 892 /* attachToRoot =*/ false); 893 kindSectionView.setIsUserProfile(mIsUserProfile); 894 895 if (Phone.CONTENT_ITEM_TYPE.equals(mimeType) 896 || Email.CONTENT_ITEM_TYPE.equals(mimeType)) { 897 // Phone numbers and email addresses are always displayed, 898 // even if they are empty 899 kindSectionView.setHideWhenEmpty(false); 900 } 901 902 // Since phone numbers and email addresses displayed even if they are empty, 903 // they will be the only types you add new values to initially for new contacts 904 kindSectionView.setShowOneEmptyEditor(true); 905 906 kindSectionView.setState(kindSectionData, mViewIdGenerator, mListener); 907 908 return kindSectionView; 909 } 910 911 private void showAllFields() { 912 // Stop hiding empty editors and allow the user to enter values for all kinds now 913 for (int i = 0; i < mKindSectionViews.getChildCount(); i++) { 914 final KindSectionView kindSectionView = 915 (KindSectionView) mKindSectionViews.getChildAt(i); 916 kindSectionView.setHideWhenEmpty(false); 917 kindSectionView.updateEmptyEditors(/* shouldAnimate =*/ true); 918 } 919 mIsExpanded = true; 920 921 // Hide the more fields button 922 mMoreFields.setVisibility(View.GONE); 923 } 924 925 private boolean hasMoreFields() { 926 for (KindSectionView section : mKindSectionViewMap.values()) { 927 if (section.getVisibility() != View.VISIBLE) { 928 return true; 929 } 930 } 931 return false; 932 } 933 934 private static void wlog(String message) { 935 if (Log.isLoggable(TAG, Log.WARN)) { 936 Log.w(TAG, message); 937 } 938 } 939 940 private static void elog(String message) { 941 Log.e(TAG, message); 942 } 943} 944