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 getPhoneticEditorView()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 getCurrentRawContactDelta()397 public RawContactDelta getCurrentRawContactDelta() { 398 return mCurrentRawContactDelta; 399 } 400 401 /** 402 * Marks the raw contact photo given as primary for the aggregate contact. 403 */ setPrimaryPhoto()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 getAggregationAnchorView()418 public View getAggregationAnchorView() { 419 final StructuredNameEditorView nameEditorView = getNameEditorView(); 420 return nameEditorView != null ? nameEditorView.findViewById(R.id.anchor_view) : null; 421 } 422 setGroupMetaData(Cursor groupMetaData)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 setIntentExtras(Bundle extras)436 public void setIntentExtras(Bundle extras) { 437 mIntentExtras = extras; 438 } 439 setState(RawContactDeltaList rawContactDeltas, MaterialColorMapUtils.MaterialPalette materialPalette, ViewIdGenerator viewIdGenerator, boolean hasNewContact, boolean isUserProfile, AccountWithDataSet primaryAccount, long rawContactIdToDisplayAlone)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 setAccounts(List<AccountInfo> accounts)523 public void setAccounts(List<AccountInfo> accounts) { 524 mAccounts.clear(); 525 mAccounts.addAll(accounts); 526 // Update the account header 527 setAccountInfo(); 528 } 529 setupEditorNormally()530 private void setupEditorNormally() { 531 addKindSectionViews(); 532 533 mMoreFields.setVisibility(hasMoreFields() ? View.VISIBLE : View.GONE); 534 535 if (mIsExpanded) showAllFields(); 536 } 537 isReadOnlyRawContact()538 private boolean isReadOnlyRawContact() { 539 return !mCurrentRawContactDelta.getAccountType(mAccountTypeManager).areContactsWritable(); 540 } 541 pickRawContactDelta()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 applyIntentExtras()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 parseRawContactDelta()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 addReadOnlyRawContactEditorViews()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 bindData(Drawable icon, String iconContentDescription, CharSequence data, CharSequence type, boolean isFirstEntry)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 bindData(Drawable icon, String iconContentDescription, CharSequence data, CharSequence type, boolean isFirstEntry, boolean forceLTR)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 setAccountInfo()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 setAccountHeader(String primaryText, String secondaryText)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 addAccountSelector(final RawContactDelta rawContactDelta)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 addPhotoView()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 addKindSectionViews()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 inflateKindSectionView(ViewGroup viewGroup, KindSectionData kindSectionData, String mimeType)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 showAllFields()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 hasMoreFields()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 wlog(String message)941 private static void wlog(String message) { 942 if (Log.isLoggable(TAG, Log.WARN)) { 943 Log.w(TAG, message); 944 } 945 } 946 elog(String message)947 private static void elog(String message) { 948 Log.e(TAG, message); 949 } 950 } 951