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 */ onNameFieldChanged(long rawContactId, ValuesDelta valuesDelta)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 */ onRebindEditorsForNewContact(RawContactDelta oldState, AccountWithDataSet oldAccount, AccountWithDataSet newAccount)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 */ onBindEditorsFailed()113 public void onBindEditorsFailed(); 114 115 /** 116 * Invoked after editors have been bound for the contact. 117 */ onEditorsBound()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 compare(String mimeType1, String mimeType2)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 SavedState(Parcelable superState)179 public SavedState(Parcelable superState) { 180 super(superState); 181 } 182 SavedState(Parcel in)183 private SavedState(Parcel in) { 184 super(in); 185 mIsExpanded = in.readInt() != 0; 186 } 187 188 @Override writeToParcel(Parcel out, int flags)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 RawContactEditorView(Context context)230 public RawContactEditorView(Context context) { 231 super(context); 232 } 233 RawContactEditorView(Context context, AttributeSet attrs)234 public RawContactEditorView(Context context, AttributeSet attrs) { 235 super(context, attrs); 236 } 237 238 /** 239 * Sets the receiver for {@link RawContactEditorView} callbacks. 240 */ setListener(Listener listener)241 public void setListener(Listener listener) { 242 mListener = listener; 243 } 244 245 @Override onFinishInflate()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 onClick(View view)267 public void onClick(View view) { 268 if (view.getId() == R.id.more_fields) { 269 showAllFields(); 270 } 271 } 272 273 @Override setEnabled(boolean enabled)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 onSaveInstanceState()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 onRestoreInstanceState(Parcelable state)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 */ setPhotoListener(PhotoEditorView.Listener listener)307 public void setPhotoListener(PhotoEditorView.Listener listener) { 308 mPhotoView.setListener(listener); 309 } 310 removePhoto()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 */ setFullSizePhoto(Uri photoUri)322 public void setFullSizePhoto(Uri photoUri) { 323 mPhotoView.setFullSizedPhoto(photoUri); 324 } 325 updatePhoto(Uri photoUri)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 unsetSuperPrimaryFromAllPhotos()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 */ isWritablePhotoSet()372 public boolean isWritablePhotoSet() { 373 return mPhotoView.isWritablePhotoSet(); 374 } 375 376 /** 377 * Get the raw contact ID for the current photo. 378 */ getPhotoRawContactId()379 public long getPhotoRawContactId() { 380 return mCurrentRawContactDelta == null ? - 1 : mCurrentRawContactDelta.getRawContactId(); 381 } 382 getNameEditorView()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 getCurrentRawContactDelta()390 public RawContactDelta getCurrentRawContactDelta() { 391 return mCurrentRawContactDelta; 392 } 393 394 /** 395 * Marks the raw contact photo given as primary for the aggregate contact. 396 */ setPrimaryPhoto()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 getAggregationAnchorView()411 public View getAggregationAnchorView() { 412 final StructuredNameEditorView nameEditorView = getNameEditorView(); 413 return nameEditorView != null ? nameEditorView.findViewById(R.id.anchor_view) : null; 414 } 415 setGroupMetaData(Cursor groupMetaData)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 setIntentExtras(Bundle extras)429 public void setIntentExtras(Bundle extras) { 430 mIntentExtras = extras; 431 } 432 setState(RawContactDeltaList rawContactDeltas, MaterialColorMapUtils.MaterialPalette materialPalette, ViewIdGenerator viewIdGenerator, boolean hasNewContact, boolean isUserProfile, AccountWithDataSet primaryAccount, long rawContactIdToDisplayAlone)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 setAccounts(List<AccountInfo> accounts)516 public void setAccounts(List<AccountInfo> accounts) { 517 mAccounts.clear(); 518 mAccounts.addAll(accounts); 519 // Update the account header 520 setAccountInfo(); 521 } 522 setupEditorNormally()523 private void setupEditorNormally() { 524 addKindSectionViews(); 525 526 mMoreFields.setVisibility(hasMoreFields() ? View.VISIBLE : View.GONE); 527 528 if (mIsExpanded) showAllFields(); 529 } 530 isReadOnlyRawContact()531 private boolean isReadOnlyRawContact() { 532 return !mCurrentRawContactDelta.getAccountType(mAccountTypeManager).areContactsWritable(); 533 } 534 pickRawContactDelta()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 applyIntentExtras()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 parseRawContactDelta()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 addReadOnlyRawContactEditorViews()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 bindData(Drawable icon, String iconContentDescription, CharSequence data, CharSequence type, boolean isFirstEntry)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 bindData(Drawable icon, String iconContentDescription, CharSequence data, CharSequence type, boolean isFirstEntry, boolean forceLTR)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 setAccountInfo()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 setAccountHeader(String primaryText, String secondaryText)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 addAccountSelector(final RawContactDelta rawContactDelta)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 addPhotoView()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 addKindSectionViews()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 inflateKindSectionView(ViewGroup viewGroup, KindSectionData kindSectionData, String mimeType)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 showAllFields()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 hasMoreFields()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 wlog(String message)934 private static void wlog(String message) { 935 if (Log.isLoggable(TAG, Log.WARN)) { 936 Log.w(TAG, message); 937 } 938 } 939 elog(String message)940 private static void elog(String message) { 941 Log.e(TAG, message); 942 } 943 } 944