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