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 com.android.contacts.R; 20 import com.android.contacts.common.model.AccountTypeManager; 21 import com.android.contacts.common.model.RawContactDelta; 22 import com.android.contacts.common.model.RawContactDeltaList; 23 import com.android.contacts.common.model.RawContactModifier; 24 import com.android.contacts.common.model.ValuesDelta; 25 import com.android.contacts.common.model.account.AccountType; 26 import com.android.contacts.common.model.account.AccountWithDataSet; 27 import com.android.contacts.common.model.dataitem.DataKind; 28 import com.android.contacts.common.util.AccountsListAdapter; 29 import com.android.contacts.common.util.MaterialColorMapUtils; 30 import com.android.contacts.util.UiClosables; 31 32 import android.content.ContentUris; 33 import android.content.Context; 34 import android.database.Cursor; 35 import android.graphics.Bitmap; 36 import android.net.Uri; 37 import android.os.Bundle; 38 import android.os.Parcel; 39 import android.os.Parcelable; 40 import android.provider.ContactsContract; 41 import android.provider.ContactsContract.CommonDataKinds.Email; 42 import android.provider.ContactsContract.CommonDataKinds.Event; 43 import android.provider.ContactsContract.CommonDataKinds.GroupMembership; 44 import android.provider.ContactsContract.CommonDataKinds.Im; 45 import android.provider.ContactsContract.CommonDataKinds.Nickname; 46 import android.provider.ContactsContract.CommonDataKinds.Note; 47 import android.provider.ContactsContract.CommonDataKinds.Organization; 48 import android.provider.ContactsContract.CommonDataKinds.Phone; 49 import android.provider.ContactsContract.CommonDataKinds.Photo; 50 import android.provider.ContactsContract.CommonDataKinds.Relation; 51 import android.provider.ContactsContract.CommonDataKinds.SipAddress; 52 import android.provider.ContactsContract.CommonDataKinds.StructuredName; 53 import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; 54 import android.provider.ContactsContract.CommonDataKinds.Website; 55 import android.text.TextUtils; 56 import android.util.AttributeSet; 57 import android.util.Log; 58 import android.util.Pair; 59 import android.view.LayoutInflater; 60 import android.view.View; 61 import android.view.ViewGroup; 62 import android.widget.AdapterView; 63 import android.widget.BaseAdapter; 64 import android.widget.ImageView; 65 import android.widget.LinearLayout; 66 import android.widget.ListPopupWindow; 67 import android.widget.TextView; 68 69 import java.io.FileNotFoundException; 70 import java.util.ArrayList; 71 import java.util.Arrays; 72 import java.util.Collections; 73 import java.util.Comparator; 74 import java.util.HashMap; 75 import java.util.List; 76 import java.util.Map; 77 import java.util.TreeSet; 78 79 /** 80 * View to display information from multiple {@link RawContactDelta}s grouped together. 81 */ 82 public class CompactRawContactsEditorView extends LinearLayout implements View.OnClickListener { 83 84 static final String TAG = "CompactEditorView"; 85 86 private static final KindSectionDataMapEntryComparator 87 KIND_SECTION_DATA_MAP_ENTRY_COMPARATOR = new KindSectionDataMapEntryComparator(); 88 89 /** 90 * Callbacks for hosts of {@link CompactRawContactsEditorView}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 compact 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 * Invoked when a rawcontact from linked contacts is selected in editor. 124 */ onRawContactSelected(Uri uri, long rawContactId, boolean isReadOnly)125 public void onRawContactSelected(Uri uri, long rawContactId, boolean isReadOnly); 126 127 /** 128 * Returns the map of raw contact IDs to newly taken or selected photos that have not 129 * yet been saved to CP2. 130 */ getUpdatedPhotos()131 public Bundle getUpdatedPhotos(); 132 } 133 134 /** 135 * Used to list the account info for the given raw contacts list. 136 */ 137 private static final class RawContactAccountListAdapter extends BaseAdapter { 138 private final LayoutInflater mInflater; 139 private final Context mContext; 140 private final RawContactDeltaList mRawContactDeltas; 141 RawContactAccountListAdapter(Context context, RawContactDeltaList rawContactDeltas)142 public RawContactAccountListAdapter(Context context, RawContactDeltaList rawContactDeltas) { 143 mContext = context; 144 mRawContactDeltas = new RawContactDeltaList(); 145 for (RawContactDelta rawContactDelta : rawContactDeltas) { 146 if (rawContactDelta.isVisible() && rawContactDelta.getRawContactId() > 0) { 147 mRawContactDeltas.add(rawContactDelta); 148 } 149 } 150 mInflater = LayoutInflater.from(context); 151 } 152 153 @Override getView(int position, View convertView, ViewGroup parent)154 public View getView(int position, View convertView, ViewGroup parent) { 155 final View resultView = convertView != null ? convertView 156 : mInflater.inflate(R.layout.account_selector_list_item, parent, false); 157 158 final RawContactDelta rawContactDelta = mRawContactDeltas.get(position); 159 160 final TextView text1 = (TextView) resultView.findViewById(android.R.id.text1); 161 final AccountType accountType = rawContactDelta.getRawContactAccountType(mContext); 162 text1.setText(accountType.getDisplayLabel(mContext)); 163 164 final TextView text2 = (TextView) resultView.findViewById(android.R.id.text2); 165 final String accountName = rawContactDelta.getAccountName(); 166 if (TextUtils.isEmpty(accountName)) { 167 text2.setVisibility(View.GONE); 168 } else { 169 // Truncate email addresses in the middle so we don't lose the domain 170 text2.setText(accountName); 171 text2.setEllipsize(TextUtils.TruncateAt.MIDDLE); 172 } 173 174 final ImageView icon = (ImageView) resultView.findViewById(android.R.id.icon); 175 icon.setImageDrawable(accountType.getDisplayIcon(mContext)); 176 177 return resultView; 178 } 179 180 @Override getCount()181 public int getCount() { 182 return mRawContactDeltas.size(); 183 } 184 185 @Override getItem(int position)186 public RawContactDelta getItem(int position) { 187 return mRawContactDeltas.get(position); 188 } 189 190 @Override getItemId(int position)191 public long getItemId(int position) { 192 return getItem(position).getRawContactId(); 193 } 194 } 195 196 /** Used to sort entire kind sections. */ 197 private static final class KindSectionDataMapEntryComparator implements 198 Comparator<Map.Entry<String,KindSectionDataList>> { 199 200 final MimeTypeComparator mMimeTypeComparator = new MimeTypeComparator(); 201 202 @Override compare(Map.Entry<String, KindSectionDataList> entry1, Map.Entry<String, KindSectionDataList> entry2)203 public int compare(Map.Entry<String, KindSectionDataList> entry1, 204 Map.Entry<String, KindSectionDataList> entry2) { 205 if (entry1 == entry2) return 0; 206 if (entry1 == null) return -1; 207 if (entry2 == null) return 1; 208 209 final String mimeType1 = entry1.getKey(); 210 final String mimeType2 = entry2.getKey(); 211 212 return mMimeTypeComparator.compare(mimeType1, mimeType2); 213 } 214 } 215 216 /** 217 * Sorts kinds roughly the same as quick contacts; we diverge in the following ways: 218 * <ol> 219 * <li>All names are together at the top.</li> 220 * <li>IM is moved up after addresses</li> 221 * <li>SIP addresses are moved to below phone numbers</li> 222 * <li>Group membership is placed at the end</li> 223 * </ol> 224 */ 225 private static final class MimeTypeComparator implements Comparator<String> { 226 227 private static final List<String> MIME_TYPE_ORDER = Arrays.asList(new String[] { 228 StructuredName.CONTENT_ITEM_TYPE, 229 Nickname.CONTENT_ITEM_TYPE, 230 Organization.CONTENT_ITEM_TYPE, 231 Phone.CONTENT_ITEM_TYPE, 232 SipAddress.CONTENT_ITEM_TYPE, 233 Email.CONTENT_ITEM_TYPE, 234 StructuredPostal.CONTENT_ITEM_TYPE, 235 Im.CONTENT_ITEM_TYPE, 236 Website.CONTENT_ITEM_TYPE, 237 Event.CONTENT_ITEM_TYPE, 238 Relation.CONTENT_ITEM_TYPE, 239 Note.CONTENT_ITEM_TYPE, 240 GroupMembership.CONTENT_ITEM_TYPE 241 }); 242 243 @Override compare(String mimeType1, String mimeType2)244 public int compare(String mimeType1, String mimeType2) { 245 if (mimeType1 == mimeType2) return 0; 246 if (mimeType1 == null) return -1; 247 if (mimeType2 == null) return 1; 248 249 int index1 = MIME_TYPE_ORDER.indexOf(mimeType1); 250 int index2 = MIME_TYPE_ORDER.indexOf(mimeType2); 251 252 // Fallback to alphabetical ordering of the mime type if both are not found 253 if (index1 < 0 && index2 < 0) return mimeType1.compareTo(mimeType2); 254 if (index1 < 0) return 1; 255 if (index2 < 0) return -1; 256 257 return index1 < index2 ? -1 : 1; 258 } 259 } 260 261 /** 262 * Sorts primary accounts and google account types before others. 263 */ 264 private static final class EditorComparator implements Comparator<KindSectionData> { 265 266 private RawContactDeltaComparator mRawContactDeltaComparator; 267 EditorComparator(Context context)268 private EditorComparator(Context context) { 269 mRawContactDeltaComparator = new RawContactDeltaComparator(context); 270 } 271 272 @Override compare(KindSectionData kindSectionData1, KindSectionData kindSectionData2)273 public int compare(KindSectionData kindSectionData1, KindSectionData kindSectionData2) { 274 if (kindSectionData1 == kindSectionData2) return 0; 275 if (kindSectionData1 == null) return -1; 276 if (kindSectionData2 == null) return 1; 277 278 final RawContactDelta rawContactDelta1 = kindSectionData1.getRawContactDelta(); 279 final RawContactDelta rawContactDelta2 = kindSectionData2.getRawContactDelta(); 280 281 if (rawContactDelta1 == rawContactDelta2) return 0; 282 if (rawContactDelta1 == null) return -1; 283 if (rawContactDelta2 == null) return 1; 284 285 return mRawContactDeltaComparator.compare(rawContactDelta1, rawContactDelta2); 286 } 287 } 288 289 public static class SavedState extends BaseSavedState { 290 291 public static final Parcelable.Creator<SavedState> CREATOR = 292 new Parcelable.Creator<SavedState>() { 293 public SavedState createFromParcel(Parcel in) { 294 return new SavedState(in); 295 } 296 public SavedState[] newArray(int size) { 297 return new SavedState[size]; 298 } 299 }; 300 301 private boolean mIsExpanded; 302 SavedState(Parcelable superState)303 public SavedState(Parcelable superState) { 304 super(superState); 305 } 306 SavedState(Parcel in)307 private SavedState(Parcel in) { 308 super(in); 309 mIsExpanded = in.readInt() != 0; 310 } 311 312 @Override writeToParcel(Parcel out, int flags)313 public void writeToParcel(Parcel out, int flags) { 314 super.writeToParcel(out, flags); 315 out.writeInt(mIsExpanded ? 1 : 0); 316 } 317 } 318 319 private CompactRawContactsEditorView.Listener mListener; 320 321 private AccountTypeManager mAccountTypeManager; 322 private LayoutInflater mLayoutInflater; 323 324 private ViewIdGenerator mViewIdGenerator; 325 private MaterialColorMapUtils.MaterialPalette mMaterialPalette; 326 private long mPhotoId = -1; 327 private boolean mHasNewContact; 328 private boolean mIsUserProfile; 329 private AccountWithDataSet mPrimaryAccount; 330 private Map<String,KindSectionDataList> mKindSectionDataMap = new HashMap<>(); 331 332 // Account header 333 private View mAccountHeaderContainer; 334 private TextView mAccountHeaderType; 335 private TextView mAccountHeaderName; 336 private ImageView mAccountHeaderIcon; 337 338 // Account selector 339 private View mAccountSelectorContainer; 340 private View mAccountSelector; 341 private TextView mAccountSelectorType; 342 private TextView mAccountSelectorName; 343 344 // Raw contacts selector 345 private View mRawContactContainer; 346 private TextView mRawContactSummary; 347 348 private CompactPhotoEditorView mPhotoView; 349 private ViewGroup mKindSectionViews; 350 private Map<String,List<CompactKindSectionView>> mKindSectionViewsMap = new HashMap<>(); 351 private View mMoreFields; 352 353 private boolean mIsExpanded; 354 355 private long mPhotoRawContactId; 356 private ValuesDelta mPhotoValuesDelta; 357 358 private Pair<KindSectionData, ValuesDelta> mPrimaryNameKindSectionData; 359 CompactRawContactsEditorView(Context context)360 public CompactRawContactsEditorView(Context context) { 361 super(context); 362 } 363 CompactRawContactsEditorView(Context context, AttributeSet attrs)364 public CompactRawContactsEditorView(Context context, AttributeSet attrs) { 365 super(context, attrs); 366 } 367 368 /** 369 * Sets the receiver for {@link CompactRawContactsEditorView} callbacks. 370 */ setListener(Listener listener)371 public void setListener(Listener listener) { 372 mListener = listener; 373 } 374 375 @Override onFinishInflate()376 protected void onFinishInflate() { 377 super.onFinishInflate(); 378 379 mAccountTypeManager = AccountTypeManager.getInstance(getContext()); 380 mLayoutInflater = (LayoutInflater) 381 getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE); 382 383 // Account header 384 mAccountHeaderContainer = findViewById(R.id.account_container); 385 mAccountHeaderType = (TextView) findViewById(R.id.account_type); 386 mAccountHeaderName = (TextView) findViewById(R.id.account_name); 387 mAccountHeaderIcon = (ImageView) findViewById(R.id.account_type_icon); 388 389 // Account selector 390 mAccountSelectorContainer = findViewById(R.id.account_selector_container); 391 mAccountSelector = findViewById(R.id.account); 392 mAccountSelectorType = (TextView) findViewById(R.id.account_type_selector); 393 mAccountSelectorName = (TextView) findViewById(R.id.account_name_selector); 394 395 // Raw contacts selector 396 mRawContactContainer = findViewById(R.id.all_rawcontacts_accounts_container); 397 mRawContactSummary = (TextView) findViewById(R.id.rawcontacts_accounts_summary); 398 399 mPhotoView = (CompactPhotoEditorView) findViewById(R.id.photo_editor); 400 mKindSectionViews = (LinearLayout) findViewById(R.id.kind_section_views); 401 mMoreFields = findViewById(R.id.more_fields); 402 mMoreFields.setOnClickListener(this); 403 } 404 405 @Override onClick(View view)406 public void onClick(View view) { 407 if (view.getId() == R.id.more_fields) { 408 showAllFields(); 409 } 410 } 411 412 @Override setEnabled(boolean enabled)413 public void setEnabled(boolean enabled) { 414 super.setEnabled(enabled); 415 final int childCount = mKindSectionViews.getChildCount(); 416 for (int i = 0; i < childCount; i++) { 417 mKindSectionViews.getChildAt(i).setEnabled(enabled); 418 } 419 } 420 421 @Override onSaveInstanceState()422 public Parcelable onSaveInstanceState() { 423 final Parcelable superState = super.onSaveInstanceState(); 424 final SavedState savedState = new SavedState(superState); 425 savedState.mIsExpanded = mIsExpanded; 426 return savedState; 427 } 428 429 @Override onRestoreInstanceState(Parcelable state)430 public void onRestoreInstanceState(Parcelable state) { 431 if(!(state instanceof SavedState)) { 432 super.onRestoreInstanceState(state); 433 return; 434 } 435 final SavedState savedState = (SavedState) state; 436 super.onRestoreInstanceState(savedState.getSuperState()); 437 mIsExpanded = savedState.mIsExpanded; 438 if (mIsExpanded) { 439 showAllFields(); 440 } 441 } 442 443 /** 444 * Pass through to {@link CompactPhotoEditorView#setListener}. 445 */ setPhotoListener(CompactPhotoEditorView.Listener listener)446 public void setPhotoListener(CompactPhotoEditorView.Listener listener) { 447 mPhotoView.setListener(listener); 448 } 449 removePhoto()450 public void removePhoto() { 451 mPhotoValuesDelta.setFromTemplate(true); 452 mPhotoValuesDelta.put(Photo.PHOTO, (byte[]) null); 453 454 mPhotoView.removePhoto(); 455 } 456 457 /** 458 * Pass through to {@link CompactPhotoEditorView#setFullSizedPhoto(Uri)}. 459 */ setFullSizePhoto(Uri photoUri)460 public void setFullSizePhoto(Uri photoUri) { 461 mPhotoView.setFullSizedPhoto(photoUri); 462 } 463 updatePhoto(Uri photoUri)464 public void updatePhoto(Uri photoUri) { 465 mPhotoValuesDelta.setFromTemplate(false); 466 // Unset primary for all photos 467 unsetSuperPrimaryFromAllPhotos(); 468 // Mark the currently displayed photo as primary 469 mPhotoValuesDelta.setSuperPrimary(true); 470 471 // Even though high-res photos cannot be saved by passing them via 472 // an EntityDeltaList (since they cause the Bundle size limit to be 473 // exceeded), we still pass a low-res thumbnail. This simplifies 474 // code all over the place, because we don't have to test whether 475 // there is a change in EITHER the delta-list OR a changed photo... 476 // this way, there is always a change in the delta-list. 477 try { 478 final byte[] bytes = EditorUiUtils.getCompressedThumbnailBitmapBytes( 479 getContext(), photoUri); 480 if (bytes != null) { 481 mPhotoValuesDelta.setPhoto(bytes); 482 } 483 } catch (FileNotFoundException e) { 484 elog("Failed to get bitmap from photo Uri"); 485 } 486 487 mPhotoView.setFullSizedPhoto(photoUri); 488 } 489 unsetSuperPrimaryFromAllPhotos()490 private void unsetSuperPrimaryFromAllPhotos() { 491 final List<KindSectionData> kindSectionDataList = 492 mKindSectionDataMap.get(Photo.CONTENT_ITEM_TYPE); 493 for (KindSectionData kindSectionData : kindSectionDataList) { 494 for (ValuesDelta valuesDelta : kindSectionData.getNonEmptyValuesDeltas()) { 495 valuesDelta.setSuperPrimary(false); 496 } 497 } 498 } 499 500 /** 501 * Pass through to {@link CompactPhotoEditorView#isWritablePhotoSet}. 502 */ isWritablePhotoSet()503 public boolean isWritablePhotoSet() { 504 return mPhotoView.isWritablePhotoSet(); 505 } 506 507 /** 508 * Get the raw contact ID for the CompactHeaderView photo. 509 */ getPhotoRawContactId()510 public long getPhotoRawContactId() { 511 return mPhotoRawContactId; 512 } 513 getPrimaryNameEditorView()514 public StructuredNameEditorView getPrimaryNameEditorView() { 515 final CompactKindSectionView primaryNameKindSectionView = getPrimaryNameKindSectionView(); 516 return primaryNameKindSectionView == null 517 ? null : primaryNameKindSectionView.getPrimaryNameEditorView(); 518 } 519 520 /** 521 * Returns a data holder for every non-default/non-empty photo from each raw contact, whether 522 * the raw contact is writable or not. 523 */ getPhotos()524 public ArrayList<CompactPhotoSelectionFragment.Photo> getPhotos() { 525 final ArrayList<CompactPhotoSelectionFragment.Photo> photos = new ArrayList<>(); 526 527 final Bundle updatedPhotos = mListener == null ? null : mListener.getUpdatedPhotos(); 528 529 final List<KindSectionData> kindSectionDataList = 530 mKindSectionDataMap.get(Photo.CONTENT_ITEM_TYPE); 531 for (int i = 0; i < kindSectionDataList.size(); i++) { 532 final KindSectionData kindSectionData = kindSectionDataList.get(i); 533 final AccountType accountType = kindSectionData.getAccountType(); 534 final List<ValuesDelta> valuesDeltas = kindSectionData.getNonEmptyValuesDeltas(); 535 if (valuesDeltas.isEmpty()) continue; 536 for (int j = 0; j < valuesDeltas.size(); j++) { 537 final ValuesDelta valuesDelta = valuesDeltas.get(j); 538 final Bitmap bitmap = EditorUiUtils.getPhotoBitmap(valuesDelta); 539 if (bitmap == null) continue; 540 541 final CompactPhotoSelectionFragment.Photo photo = 542 new CompactPhotoSelectionFragment.Photo(); 543 photo.titleRes = accountType.titleRes; 544 photo.iconRes = accountType.iconRes; 545 photo.syncAdapterPackageName = accountType.syncAdapterPackageName; 546 photo.valuesDelta = valuesDelta; 547 photo.primary = valuesDelta.isSuperPrimary(); 548 photo.kindSectionDataListIndex = i; 549 photo.valuesDeltaListIndex = j; 550 photo.photoId = valuesDelta.getId(); 551 552 if (updatedPhotos != null) { 553 photo.updatedPhotoUri = (Uri) updatedPhotos.get(String.valueOf( 554 kindSectionData.getRawContactDelta().getRawContactId())); 555 } 556 557 final CharSequence accountTypeLabel = accountType.getDisplayLabel(getContext()); 558 photo.accountType = accountTypeLabel == null ? "" : accountTypeLabel.toString(); 559 final String accountName = kindSectionData.getRawContactDelta().getAccountName(); 560 photo.accountName = accountName == null ? "" : accountName; 561 562 photos.add(photo); 563 } 564 } 565 566 return photos; 567 } 568 569 /** 570 * Marks the raw contact photo given as primary for the aggregate contact and updates the 571 * UI. 572 */ setPrimaryPhoto(CompactPhotoSelectionFragment.Photo photo)573 public void setPrimaryPhoto(CompactPhotoSelectionFragment.Photo photo) { 574 // Find the values delta to mark as primary 575 final KindSectionDataList kindSectionDataList = 576 mKindSectionDataMap.get(Photo.CONTENT_ITEM_TYPE); 577 if (photo.kindSectionDataListIndex < 0 578 || photo.kindSectionDataListIndex >= kindSectionDataList.size()) { 579 wlog("Invalid kind section data list index"); 580 return; 581 } 582 final KindSectionData kindSectionData = 583 kindSectionDataList.get(photo.kindSectionDataListIndex); 584 final List<ValuesDelta> valuesDeltaList = kindSectionData.getNonEmptyValuesDeltas(); 585 if (photo.valuesDeltaListIndex >= valuesDeltaList.size()) { 586 wlog("Invalid values delta list index"); 587 return; 588 } 589 590 // Update values delta 591 final ValuesDelta valuesDelta = valuesDeltaList.get(photo.valuesDeltaListIndex); 592 valuesDelta.setFromTemplate(false); 593 unsetSuperPrimaryFromAllPhotos(); 594 valuesDelta.setSuperPrimary(true); 595 596 // Update the UI 597 mPhotoView.setPhoto(valuesDelta, mMaterialPalette); 598 } 599 getAggregationAnchorView()600 public View getAggregationAnchorView() { 601 final List<CompactKindSectionView> kindSectionViews = getKindSectionViews( 602 StructuredName.CONTENT_ITEM_TYPE); 603 if (!kindSectionViews.isEmpty()) { 604 return mKindSectionViews.getChildAt(0).findViewById(R.id.anchor_view); 605 } 606 return null; 607 } 608 setGroupMetaData(Cursor groupMetaData)609 public void setGroupMetaData(Cursor groupMetaData) { 610 final List<CompactKindSectionView> kindSectionViews = getKindSectionViews( 611 GroupMembership.CONTENT_ITEM_TYPE); 612 for (CompactKindSectionView kindSectionView : kindSectionViews) { 613 kindSectionView.setGroupMetaData(groupMetaData); 614 if (mIsExpanded) { 615 kindSectionView.setHideWhenEmpty(false); 616 kindSectionView.updateEmptyEditors(/* shouldAnimate =*/ true); 617 } 618 } 619 } 620 setState(RawContactDeltaList rawContactDeltas, MaterialColorMapUtils.MaterialPalette materialPalette, ViewIdGenerator viewIdGenerator, long photoId, boolean hasNewContact, boolean isUserProfile, AccountWithDataSet primaryAccount)621 public void setState(RawContactDeltaList rawContactDeltas, 622 MaterialColorMapUtils.MaterialPalette materialPalette, ViewIdGenerator viewIdGenerator, 623 long photoId, boolean hasNewContact, boolean isUserProfile, 624 AccountWithDataSet primaryAccount) { 625 mKindSectionDataMap.clear(); 626 mKindSectionViews.removeAllViews(); 627 mMoreFields.setVisibility(View.VISIBLE); 628 629 mMaterialPalette = materialPalette; 630 mViewIdGenerator = viewIdGenerator; 631 mPhotoId = photoId; 632 633 mHasNewContact = hasNewContact; 634 mIsUserProfile = isUserProfile; 635 mPrimaryAccount = primaryAccount; 636 if (mPrimaryAccount == null) { 637 mPrimaryAccount = ContactEditorUtils.getInstance(getContext()).getDefaultAccount(); 638 } 639 vlog("state: primary " + mPrimaryAccount); 640 641 // Parse the given raw contact deltas 642 if (rawContactDeltas == null || rawContactDeltas.isEmpty()) { 643 elog("No raw contact deltas"); 644 if (mListener != null) mListener.onBindEditorsFailed(); 645 return; 646 } 647 parseRawContactDeltas(rawContactDeltas); 648 if (mKindSectionDataMap.isEmpty()) { 649 elog("No kind section data parsed from RawContactDelta(s)"); 650 if (mListener != null) mListener.onBindEditorsFailed(); 651 return; 652 } 653 654 // Get the primary name kind section data 655 mPrimaryNameKindSectionData = mKindSectionDataMap.get(StructuredName.CONTENT_ITEM_TYPE) 656 .getEntryToWrite(/* id =*/ -1, mPrimaryAccount, mIsUserProfile); 657 if (mPrimaryNameKindSectionData != null) { 658 // Ensure that a structured name and photo exists 659 final RawContactDelta rawContactDelta = 660 mPrimaryNameKindSectionData.first.getRawContactDelta(); 661 RawContactModifier.ensureKindExists( 662 rawContactDelta, 663 rawContactDelta.getAccountType(mAccountTypeManager), 664 StructuredName.CONTENT_ITEM_TYPE); 665 RawContactModifier.ensureKindExists( 666 rawContactDelta, 667 rawContactDelta.getAccountType(mAccountTypeManager), 668 Photo.CONTENT_ITEM_TYPE); 669 } 670 671 // Setup the view 672 addAccountInfo(rawContactDeltas); 673 addPhotoView(); 674 addKindSectionViews(); 675 676 if (mIsExpanded) showAllFields(); 677 678 if (mListener != null) mListener.onEditorsBound(); 679 } 680 parseRawContactDeltas(RawContactDeltaList rawContactDeltas)681 private void parseRawContactDeltas(RawContactDeltaList rawContactDeltas) { 682 // Build the kind section data list map 683 vlog("parse: " + rawContactDeltas.size() + " rawContactDelta(s)"); 684 for (int j = 0; j < rawContactDeltas.size(); j++) { 685 final RawContactDelta rawContactDelta = rawContactDeltas.get(j); 686 vlog("parse: " + j + " rawContactDelta" + rawContactDelta); 687 if (rawContactDelta == null || !rawContactDelta.isVisible()) continue; 688 final AccountType accountType = rawContactDelta.getAccountType(mAccountTypeManager); 689 if (accountType == null) continue; 690 final List<DataKind> dataKinds = accountType.getSortedDataKinds(); 691 final int dataKindSize = dataKinds == null ? 0 : dataKinds.size(); 692 vlog("parse: " + dataKindSize + " dataKinds(s)"); 693 for (int i = 0; i < dataKindSize; i++) { 694 final DataKind dataKind = dataKinds.get(i); 695 if (dataKind == null || !dataKind.editable) { 696 vlog("parse: " + i + " " + dataKind.mimeType + " dropped read-only"); 697 continue; 698 } 699 final String mimeType = dataKind.mimeType; 700 701 // Skip psuedo mime types 702 if (DataKind.PSEUDO_MIME_TYPE_DISPLAY_NAME.equals(mimeType) 703 || DataKind.PSEUDO_MIME_TYPE_PHONETIC_NAME.equals(mimeType)) { 704 vlog("parse: " + i + " " + dataKind.mimeType + " dropped pseudo type"); 705 continue; 706 } 707 708 final KindSectionDataList kindSectionDataList = 709 getOrCreateKindSectionDataList(mimeType); 710 final KindSectionData kindSectionData = 711 new KindSectionData(accountType, dataKind, rawContactDelta); 712 kindSectionDataList.add(kindSectionData); 713 714 vlog("parse: " + i + " " + dataKind.mimeType + " " + 715 kindSectionData.getValuesDeltas().size() + " value(s) " + 716 kindSectionData.getNonEmptyValuesDeltas().size() + " non-empty value(s) " + 717 kindSectionData.getVisibleValuesDeltas().size() + 718 " visible value(s)"); 719 } 720 } 721 } 722 getOrCreateKindSectionDataList(String mimeType)723 private KindSectionDataList getOrCreateKindSectionDataList(String mimeType) { 724 KindSectionDataList kindSectionDataList = mKindSectionDataMap.get(mimeType); 725 if (kindSectionDataList == null) { 726 kindSectionDataList = new KindSectionDataList(); 727 mKindSectionDataMap.put(mimeType, kindSectionDataList); 728 } 729 return kindSectionDataList; 730 } 731 addAccountInfo(RawContactDeltaList rawContactDeltas)732 private void addAccountInfo(RawContactDeltaList rawContactDeltas) { 733 mAccountHeaderContainer.setVisibility(View.GONE); 734 mAccountSelectorContainer.setVisibility(View.GONE); 735 mRawContactContainer.setVisibility(View.GONE); 736 737 if (mPrimaryNameKindSectionData == null) return; 738 final RawContactDelta rawContactDelta = 739 mPrimaryNameKindSectionData.first.getRawContactDelta(); 740 741 // Get the account information for the primary raw contact delta 742 final Pair<String,String> accountInfo = mIsUserProfile 743 ? EditorUiUtils.getLocalAccountInfo(getContext(), 744 rawContactDelta.getAccountName(), 745 rawContactDelta.getAccountType(mAccountTypeManager)) 746 : EditorUiUtils.getAccountInfo(getContext(), 747 rawContactDelta.getAccountName(), 748 rawContactDelta.getAccountType(mAccountTypeManager)); 749 750 // Either the account header or selector should be shown, not both. 751 final List<AccountWithDataSet> accounts = 752 AccountTypeManager.getInstance(getContext()).getAccounts(true); 753 if (mHasNewContact && !mIsUserProfile) { 754 if (accounts.size() > 1) { 755 addAccountSelector(accountInfo, rawContactDelta); 756 } else { 757 addAccountHeader(accountInfo); 758 } 759 } else if (mIsUserProfile || !shouldHideAccountContainer(rawContactDeltas)) { 760 addAccountHeader(accountInfo); 761 } 762 763 // The raw contact selector should only display linked raw contacts that can be edited in 764 // the full editor (i.e. they are not newly created raw contacts) 765 final RawContactAccountListAdapter adapter = new RawContactAccountListAdapter(getContext(), 766 getRawContactDeltaListForSelector(rawContactDeltas)); 767 if (adapter.getCount() > 0) { 768 final String accountsSummary = getResources().getQuantityString( 769 R.plurals.compact_editor_linked_contacts_selector_title, 770 adapter.getCount(), adapter.getCount()); 771 addRawContactAccountSelector(accountsSummary, adapter); 772 } 773 } 774 getRawContactDeltaListForSelector( RawContactDeltaList rawContactDeltas)775 private RawContactDeltaList getRawContactDeltaListForSelector( 776 RawContactDeltaList rawContactDeltas) { 777 // Sort raw contacts so google accounts come first 778 Collections.sort(rawContactDeltas, new RawContactDeltaComparator(getContext())); 779 780 final RawContactDeltaList result = new RawContactDeltaList(); 781 for (RawContactDelta rawContactDelta : rawContactDeltas) { 782 if (rawContactDelta.isVisible() && rawContactDelta.getRawContactId() > 0) { 783 // Only add raw contacts that can be opened in the editor 784 result.add(rawContactDelta); 785 } 786 } 787 // Don't return a list of size 1 that would just open the raw contact being edited 788 // in the compact editor in the full editor 789 if (result.size() == 1 && result.get(0).getRawContactAccountType( 790 getContext()).areContactsWritable()) { 791 result.clear(); 792 return result; 793 } 794 return result; 795 } 796 797 // Returns true if there are multiple writable rawcontacts and no read-only ones, 798 // or there are both writable and read-only rawcontacts. shouldHideAccountContainer(RawContactDeltaList rawContactDeltas)799 private boolean shouldHideAccountContainer(RawContactDeltaList rawContactDeltas) { 800 int writable = 0; 801 int readonly = 0; 802 for (RawContactDelta rawContactDelta : rawContactDeltas) { 803 if (rawContactDelta.isVisible() && rawContactDelta.getRawContactId() > 0) { 804 if (rawContactDelta.getRawContactAccountType(getContext()).areContactsWritable()) { 805 writable++; 806 } else { 807 readonly++; 808 } 809 } 810 } 811 return (writable > 1 || (writable > 0 && readonly > 0)); 812 } 813 addAccountHeader(Pair<String,String> accountInfo)814 private void addAccountHeader(Pair<String,String> accountInfo) { 815 mAccountHeaderContainer.setVisibility(View.VISIBLE); 816 817 // Set the account name 818 final String accountName = TextUtils.isEmpty(accountInfo.first) 819 ? accountInfo.second : accountInfo.first; 820 mAccountHeaderName.setVisibility(View.VISIBLE); 821 mAccountHeaderName.setText(accountName); 822 823 // Set the account type 824 final String selectorTitle = getResources().getString( 825 R.string.compact_editor_account_selector_title); 826 mAccountHeaderType.setText(selectorTitle); 827 828 // Set the icon 829 if (mPrimaryNameKindSectionData != null) { 830 final RawContactDelta rawContactDelta = 831 mPrimaryNameKindSectionData.first.getRawContactDelta(); 832 if (rawContactDelta != null) { 833 final AccountType accountType = 834 rawContactDelta.getRawContactAccountType(getContext()); 835 mAccountHeaderIcon.setImageDrawable(accountType.getDisplayIcon(getContext())); 836 } 837 } 838 839 // Set the content description 840 mAccountHeaderContainer.setContentDescription( 841 EditorUiUtils.getAccountInfoContentDescription(accountName, selectorTitle)); 842 } 843 addAccountSelector(Pair<String,String> accountInfo, final RawContactDelta rawContactDelta)844 private void addAccountSelector(Pair<String,String> accountInfo, 845 final RawContactDelta rawContactDelta) { 846 mAccountSelectorContainer.setVisibility(View.VISIBLE); 847 848 if (TextUtils.isEmpty(accountInfo.first)) { 849 // Hide this view so the other text view will be centered vertically 850 mAccountSelectorName.setVisibility(View.GONE); 851 } else { 852 mAccountSelectorName.setVisibility(View.VISIBLE); 853 mAccountSelectorName.setText(accountInfo.first); 854 } 855 856 final String selectorTitle = getResources().getString( 857 R.string.compact_editor_account_selector_title); 858 mAccountSelectorType.setText(selectorTitle); 859 860 mAccountSelectorContainer.setContentDescription(getResources().getString( 861 R.string.compact_editor_account_selector_description, accountInfo.first)); 862 863 mAccountSelectorContainer.setOnClickListener(new View.OnClickListener() { 864 @Override 865 public void onClick(View v) { 866 final ListPopupWindow popup = new ListPopupWindow(getContext(), null); 867 final AccountsListAdapter adapter = 868 new AccountsListAdapter(getContext(), 869 AccountsListAdapter.AccountListFilter.ACCOUNTS_CONTACT_WRITABLE, 870 mPrimaryAccount); 871 popup.setWidth(mAccountSelectorContainer.getWidth()); 872 popup.setAnchorView(mAccountSelectorContainer); 873 popup.setAdapter(adapter); 874 popup.setModal(true); 875 popup.setInputMethodMode(ListPopupWindow.INPUT_METHOD_NOT_NEEDED); 876 popup.setOnItemClickListener(new AdapterView.OnItemClickListener() { 877 @Override 878 public void onItemClick(AdapterView<?> parent, View view, int position, 879 long id) { 880 UiClosables.closeQuietly(popup); 881 final AccountWithDataSet newAccount = adapter.getItem(position); 882 if (mListener != null && !mPrimaryAccount.equals(newAccount)) { 883 mListener.onRebindEditorsForNewContact( 884 rawContactDelta, 885 mPrimaryAccount, 886 newAccount); 887 } 888 } 889 }); 890 popup.show(); 891 } 892 }); 893 } 894 addRawContactAccountSelector(String accountsSummary, final RawContactAccountListAdapter adapter)895 private void addRawContactAccountSelector(String accountsSummary, 896 final RawContactAccountListAdapter adapter) { 897 mRawContactContainer.setVisibility(View.VISIBLE); 898 899 mRawContactSummary.setText(accountsSummary); 900 901 mRawContactContainer.setOnClickListener(new View.OnClickListener() { 902 @Override 903 public void onClick(View v) { 904 final ListPopupWindow popup = new ListPopupWindow(getContext(), null); 905 popup.setWidth(mRawContactContainer.getWidth()); 906 popup.setAnchorView(mRawContactContainer); 907 popup.setAdapter(adapter); 908 popup.setModal(true); 909 popup.setInputMethodMode(ListPopupWindow.INPUT_METHOD_NOT_NEEDED); 910 popup.setOnItemClickListener(new AdapterView.OnItemClickListener() { 911 @Override 912 public void onItemClick(AdapterView<?> parent, View view, int position, 913 long id) { 914 UiClosables.closeQuietly(popup); 915 916 if (mListener != null) { 917 final long rawContactId = adapter.getItemId(position); 918 final Uri rawContactUri = ContentUris.withAppendedId( 919 ContactsContract.RawContacts.CONTENT_URI, rawContactId); 920 final RawContactDelta rawContactDelta = adapter.getItem(position); 921 final AccountTypeManager accountTypes = AccountTypeManager.getInstance( 922 getContext()); 923 final AccountType accountType = rawContactDelta.getAccountType( 924 accountTypes); 925 final boolean isReadOnly = !accountType.areContactsWritable(); 926 927 mListener.onRawContactSelected(rawContactUri, rawContactId, isReadOnly); 928 } 929 } 930 }); 931 popup.show(); 932 } 933 }); 934 } 935 addPhotoView()936 private void addPhotoView() { 937 // Get the kind section data and values delta that we will display in the photo view 938 final KindSectionDataList kindSectionDataList = 939 mKindSectionDataMap.get(Photo.CONTENT_ITEM_TYPE); 940 final Pair<KindSectionData,ValuesDelta> photoToDisplay = 941 kindSectionDataList.getEntryToDisplay(mPhotoId); 942 if (photoToDisplay == null) { 943 wlog("photo: no kind section data parsed"); 944 mPhotoView.setVisibility(View.GONE); 945 return; 946 } 947 948 // Set the photo view 949 mPhotoView.setPhoto(photoToDisplay.second, mMaterialPalette); 950 951 // Find the raw contact ID and values delta that will be written when the photo is edited 952 final Pair<KindSectionData, ValuesDelta> photoToWrite = kindSectionDataList.getEntryToWrite( 953 mPhotoId, mPrimaryAccount, mIsUserProfile); 954 if (photoToWrite == null) { 955 mPhotoView.setReadOnly(true); 956 return; 957 } 958 mPhotoView.setReadOnly(false); 959 mPhotoRawContactId = photoToWrite.first.getRawContactDelta().getRawContactId(); 960 mPhotoValuesDelta = photoToWrite.second; 961 } 962 addKindSectionViews()963 private void addKindSectionViews() { 964 // Sort the kinds 965 final TreeSet<Map.Entry<String,KindSectionDataList>> entries = 966 new TreeSet<>(KIND_SECTION_DATA_MAP_ENTRY_COMPARATOR); 967 entries.addAll(mKindSectionDataMap.entrySet()); 968 969 vlog("kind: " + entries.size() + " kindSection(s)"); 970 int i = -1; 971 for (Map.Entry<String, KindSectionDataList> entry : entries) { 972 i++; 973 974 final String mimeType = entry.getKey(); 975 976 if (StructuredName.CONTENT_ITEM_TYPE.equals(mimeType)) { 977 if (mPrimaryNameKindSectionData == null) { 978 vlog("kind: " + i + " " + mimeType + " dropped"); 979 continue; 980 } 981 vlog("kind: " + i + " " + mimeType + " using first entry only"); 982 final KindSectionDataList kindSectionDataList = new KindSectionDataList(); 983 kindSectionDataList.add(mPrimaryNameKindSectionData.first); 984 final CompactKindSectionView kindSectionView = inflateKindSectionView( 985 mKindSectionViews, kindSectionDataList, mimeType, 986 mPrimaryNameKindSectionData.second); 987 mKindSectionViews.addView(kindSectionView); 988 989 // Keep a pointer to all the KindSectionsViews for each mimeType 990 getKindSectionViews(mimeType).add(kindSectionView); 991 } else { 992 final KindSectionDataList kindSectionDataList = entry.getValue(); 993 994 // Ignore mime types that we've already handled 995 if (Photo.CONTENT_ITEM_TYPE.equals(mimeType)) { 996 vlog("kind: " + i + " " + mimeType + " dropped"); 997 continue; 998 } 999 1000 // Don't show more than one group editor on the compact editor. 1001 // Groups will still be editable for each raw contact individually on the full editor. 1002 if (GroupMembership.CONTENT_ITEM_TYPE.equals(mimeType) 1003 && kindSectionDataList.size() > 1) { 1004 vlog("kind: " + i + " " + mimeType + " dropped"); 1005 continue; 1006 } 1007 1008 if (kindSectionDataList != null && !kindSectionDataList.isEmpty()) { 1009 vlog("kind: " + i + " " + mimeType + " " + kindSectionDataList.size() + 1010 " kindSectionData(s)"); 1011 1012 final CompactKindSectionView kindSectionView = inflateKindSectionView( 1013 mKindSectionViews, kindSectionDataList, mimeType, 1014 /* primaryValueDelta =*/ null); 1015 mKindSectionViews.addView(kindSectionView); 1016 1017 // Keep a pointer to all the KindSectionsViews for each mimeType 1018 getKindSectionViews(mimeType).add(kindSectionView); 1019 } 1020 } 1021 } 1022 } 1023 getKindSectionViews(String mimeType)1024 private List<CompactKindSectionView> getKindSectionViews(String mimeType) { 1025 List<CompactKindSectionView> kindSectionViews = mKindSectionViewsMap.get(mimeType); 1026 if (kindSectionViews == null) { 1027 kindSectionViews = new ArrayList<>(); 1028 mKindSectionViewsMap.put(mimeType, kindSectionViews); 1029 } 1030 return kindSectionViews; 1031 } 1032 inflateKindSectionView(ViewGroup viewGroup, KindSectionDataList kindSectionDataList, String mimeType, ValuesDelta primaryValuesDelta)1033 private CompactKindSectionView inflateKindSectionView(ViewGroup viewGroup, 1034 KindSectionDataList kindSectionDataList, String mimeType, 1035 ValuesDelta primaryValuesDelta) { 1036 final CompactKindSectionView kindSectionView = (CompactKindSectionView) 1037 mLayoutInflater.inflate(R.layout.compact_item_kind_section, viewGroup, 1038 /* attachToRoot =*/ false); 1039 kindSectionView.setIsUserProfile(mIsUserProfile); 1040 1041 if (Phone.CONTENT_ITEM_TYPE.equals(mimeType) 1042 || Email.CONTENT_ITEM_TYPE.equals(mimeType)) { 1043 // Phone numbers and email addresses are always displayed, 1044 // even if they are empty 1045 kindSectionView.setHideWhenEmpty(false); 1046 } 1047 1048 // Since phone numbers and email addresses displayed even if they are empty, 1049 // they will be the only types you add new values to initially for new contacts 1050 kindSectionView.setShowOneEmptyEditor(true); 1051 1052 // Sort non-name editors so they wind up in the order we want 1053 if (!StructuredName.CONTENT_ITEM_TYPE.equals(mimeType)) { 1054 Collections.sort(kindSectionDataList, new EditorComparator(getContext())); 1055 } 1056 1057 kindSectionView.setState(kindSectionDataList, mViewIdGenerator, mListener, 1058 primaryValuesDelta); 1059 1060 return kindSectionView; 1061 } 1062 maybeSetReadOnlyDisplayNameAsPrimary(String readOnlyDisplayName)1063 void maybeSetReadOnlyDisplayNameAsPrimary(String readOnlyDisplayName) { 1064 if (TextUtils.isEmpty(readOnlyDisplayName)) return; 1065 final CompactKindSectionView primaryNameKindSectionView = getPrimaryNameKindSectionView(); 1066 if (primaryNameKindSectionView != null && primaryNameKindSectionView.isEmptyName()) { 1067 vlog("name: using read only display name as primary name"); 1068 primaryNameKindSectionView.setName(readOnlyDisplayName); 1069 } 1070 } 1071 getPrimaryNameKindSectionView()1072 private CompactKindSectionView getPrimaryNameKindSectionView() { 1073 final List<CompactKindSectionView> kindSectionViews 1074 = mKindSectionViewsMap.get(StructuredName.CONTENT_ITEM_TYPE); 1075 return kindSectionViews == null || kindSectionViews.isEmpty() 1076 ? null : kindSectionViews.get(0); 1077 } 1078 showAllFields()1079 private void showAllFields() { 1080 // Stop hiding empty editors and allow the user to enter values for all kinds now 1081 for (int i = 0; i < mKindSectionViews.getChildCount(); i++) { 1082 final CompactKindSectionView kindSectionView = 1083 (CompactKindSectionView) mKindSectionViews.getChildAt(i); 1084 kindSectionView.setHideWhenEmpty(false); 1085 kindSectionView.updateEmptyEditors(/* shouldAnimate =*/ true); 1086 } 1087 mIsExpanded = true; 1088 1089 // Hide the more fields button 1090 mMoreFields.setVisibility(View.GONE); 1091 } 1092 vlog(String message)1093 private static void vlog(String message) { 1094 if (Log.isLoggable(TAG, Log.VERBOSE)) { 1095 Log.v(TAG, message); 1096 } 1097 } 1098 wlog(String message)1099 private static void wlog(String message) { 1100 if (Log.isLoggable(TAG, Log.WARN)) { 1101 Log.w(TAG, message); 1102 } 1103 } 1104 elog(String message)1105 private static void elog(String message) { 1106 Log.e(TAG, message); 1107 } 1108 } 1109