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