/** Mark this entire object deleted, including any {@link ValuesDelta}. */
 public void markDeleted() {
   this.mValues.markDeleted();
   for (ArrayList<ValuesDelta> mimeEntries : mEntries.values()) {
     for (ValuesDelta child : mimeEntries) {
       child.markDeleted();
     }
   }
 }
 /**
  * Build an {@link RawContactDelta} using the given {@link RawContact} as a starting point; the
  * "before" snapshot.
  */
 public static RawContactDelta fromBefore(RawContact before) {
   final RawContactDelta rawContactDelta = new RawContactDelta();
   rawContactDelta.mValues = ValuesDelta.fromBefore(before.getValues());
   rawContactDelta.mValues.setIdColumn(RawContacts._ID);
   for (final ContentValues values : before.getContentValues()) {
     rawContactDelta.addEntry(ValuesDelta.fromBefore(values));
   }
   return rawContactDelta;
 }
 private boolean containsEntry(ValuesDelta entry) {
   for (ArrayList<ValuesDelta> mimeEntries : mEntries.values()) {
     for (ValuesDelta child : mimeEntries) {
       // Contained if we find any child that matches
       if (child.equals(entry)) return true;
     }
   }
   return false;
 }
 /** Tests whether the given item has no changes (so it exists in the database) but is empty */
 private boolean isEmptyNoop(ValuesDelta item) {
   if (!item.isNoop()) return false;
   final int fieldCount = mKind.fieldList.size();
   for (int i = 0; i < fieldCount; i++) {
     final String column = mKind.fieldList.get(i).column;
     final String value = item.getAsString(column);
     if (!TextUtils.isEmpty(value)) return false;
   }
   return true;
 }
 public ArrayList<ContentValues> getContentValues() {
   ArrayList<ContentValues> values = Lists.newArrayList();
   for (ArrayList<ValuesDelta> mimeEntries : mEntries.values()) {
     for (ValuesDelta entry : mimeEntries) {
       if (!entry.isDelete()) {
         values.add(entry.getCompleteValues());
       }
     }
   }
   return values;
 }
  public int getMimeEntriesCount(String mimeType, boolean onlyVisible) {
    final ArrayList<ValuesDelta> mimeEntries = getMimeEntries(mimeType);
    if (mimeEntries == null) return 0;

    int count = 0;
    for (ValuesDelta child : mimeEntries) {
      // Skip deleted items when requesting only visible
      if (onlyVisible && !child.isVisible()) continue;
      count++;
    }
    return count;
  }
  /**
   * Get the {@link ValuesDelta} child marked as {@link Data#IS_PRIMARY}, which may return null when
   * no entry exists.
   */
  public ValuesDelta getPrimaryEntry(String mimeType) {
    final ArrayList<ValuesDelta> mimeEntries = getMimeEntries(mimeType, false);
    if (mimeEntries == null) return null;

    for (ValuesDelta entry : mimeEntries) {
      if (entry.isPrimary()) {
        return entry;
      }
    }

    // When no direct primary, return something
    return mimeEntries.size() > 0 ? mimeEntries.get(0) : null;
  }
 private Builder buildAssertHelper() {
   final boolean isContactInsert = mValues.isInsert();
   ContentProviderOperation.Builder builder = null;
   if (!isContactInsert) {
     // Assert version is consistent while persisting changes
     final Long beforeId = mValues.getId();
     final Long beforeVersion = mValues.getAsLong(RawContacts.VERSION);
     if (beforeId == null || beforeVersion == null) return builder;
     builder = ContentProviderOperation.newAssertQuery(mContactsQueryUri);
     builder.withSelection(RawContacts._ID + "=" + beforeId, null);
     builder.withValue(RawContacts.VERSION, beforeVersion);
   }
   return builder;
 }
  /** Find entry with the given {@link BaseColumns#_ID} value. */
  public ValuesDelta getEntry(Long childId) {
    if (childId == null) {
      // Requesting an "insert" entry, which has no "before"
      return null;
    }

    // Search all children for requested entry
    for (ArrayList<ValuesDelta> mimeEntries : mEntries.values()) {
      for (ValuesDelta entry : mimeEntries) {
        if (childId.equals(entry.getId())) {
          return entry;
        }
      }
    }
    return null;
  }
  /**
   * Prepare this editor using the given {@link DataKind} for defining structure and {@link
   * ValuesDelta} describing the content to edit.
   */
  @Override
  public void setValues(
      DataKind kind,
      ValuesDelta entry,
      RawContactDelta state,
      boolean readOnly,
      ViewIdGenerator vig) {
    mKind = kind;
    mEntry = entry;
    mState = state;
    mReadOnly = readOnly;
    mViewIdGenerator = vig;
    setId(vig.getId(state, kind, entry, ViewIdGenerator.NO_VIEW_INDEX));

    if (!entry.isVisible()) {
      // Hide ourselves entirely if deleted
      setVisibility(View.GONE);
      return;
    }
    setVisibility(View.VISIBLE);

    // Display label selector if multiple types available
    final boolean hasTypes = RawContactModifier.hasEditTypes(kind);
    setupLabelButton(hasTypes);
    mLabel.setEnabled(!readOnly && isEnabled());
    if (hasTypes) {
      mType = RawContactModifier.getCurrentType(entry, kind);
      rebuildLabel();
    }
  }
  @Override
  public void deleteEditor() {
    // Keep around in model, but mark as deleted
    mEntry.markDeleted();

    // Remove the view
    EditorAnimator.getInstance().removeEditorView(this);
  }
 protected boolean isFieldChanged(String column, String value) {
   final String dbValue = mEntry.getAsString(column);
   // nullable fields (e.g. Middle Name) are usually represented as empty columns,
   // so lets treat null and empty space equivalently here
   final String dbValueNoNull = dbValue == null ? "" : dbValue;
   final String valueNoNull = value == null ? "" : value;
   return !TextUtils.equals(dbValueNoNull, valueNoNull);
 }
  /** Build editors for all current {@link #mState} rows. */
  public void rebuildFromState() {
    // Remove any existing editors
    mEditors.removeAllViews();

    // Check if we are displaying anything here
    boolean hasEntries = mState.hasMimeEntries(mKind.mimeType);

    if (hasEntries) {
      for (ValuesDelta entry : mState.getMimeEntries(mKind.mimeType)) {
        // Skip entries that aren't visible
        if (!entry.isVisible()) continue;
        if (isEmptyNoop(entry)) continue;

        createEditorView(entry);
      }
    }
  }
  private boolean hasMembership(long groupId) {
    if (groupId == mDefaultGroupId && mState.isContactInsert()) {
      return true;
    }

    ArrayList<ValuesDelta> entries = mState.getMimeEntries(GroupMembership.CONTENT_ITEM_TYPE);
    if (entries != null) {
      for (ValuesDelta values : entries) {
        if (!values.isDelete()) {
          Long id = values.getGroupRowId();
          if (id != null && id == groupId) {
            return true;
          }
        }
      }
    }
    return false;
  }
 @Override
 public String toString() {
   final StringBuilder builder = new StringBuilder();
   builder.append("\n(");
   builder.append("Uri=");
   builder.append(mContactsQueryUri);
   builder.append(", Values=");
   builder.append(mValues != null ? mValues.toString() : "null");
   builder.append(", Entries={");
   for (ArrayList<ValuesDelta> mimeEntries : mEntries.values()) {
     for (ValuesDelta child : mimeEntries) {
       builder.append("\n\t");
       child.toString(builder);
     }
   }
   builder.append("\n})\n");
   return builder.toString();
 }
  /**
   * Returns the super-primary entry for the given mime type
   *
   * @param forceSelection if true, will try to return some value even if a super-primary doesn't
   *     exist (may be a primary, or just a random item
   * @return
   */
  @NeededForTesting
  public ValuesDelta getSuperPrimaryEntry(String mimeType, boolean forceSelection) {
    final ArrayList<ValuesDelta> mimeEntries = getMimeEntries(mimeType, false);
    if (mimeEntries == null) return null;

    ValuesDelta primary = null;
    for (ValuesDelta entry : mimeEntries) {
      if (entry.isSuperPrimary()) {
        return entry;
      } else if (entry.isPrimary()) {
        primary = entry;
      }
    }

    if (!forceSelection) {
      return null;
    }

    // When no direct super primary, return something
    if (primary != null) {
      return primary;
    }
    return mimeEntries.size() > 0 ? mimeEntries.get(0) : null;
  }
  protected void onTypeSelectionChange(int position) {
    EditType selected = mEditTypeAdapter.getItem(position);
    // See if the selection has in fact changed
    if (mEditTypeAdapter.hasCustomSelection() && selected == CUSTOM_SELECTION) {
      return;
    }

    if (mType == selected && mType.customColumn == null) {
      return;
    }

    if (selected.customColumn != null) {
      showDialog(DIALOG_ID_CUSTOM);
    } else {
      // User picked type, and we're sure it's ok to actually write the entry.
      mType = selected;
      mEntry.put(mKind.typeColumn, mType.rawValue);
      rebuildLabel();
      requestFocusForFirstEditField();
      onLabelRebuilt();
    }
  }
  private static String getMapKey(
      RawContactDelta entity, DataKind kind, ValuesDelta values, int viewIndex) {
    sWorkStringBuilder.setLength(0);
    if (entity != null) {
      sWorkStringBuilder.append(entity.getValues().getId());

      if (kind != null) {
        sWorkStringBuilder.append(KEY_SEPARATOR);
        sWorkStringBuilder.append(kind.mimeType);

        if (values != null) {
          sWorkStringBuilder.append(KEY_SEPARATOR);
          sWorkStringBuilder.append(values.getId());

          if (viewIndex != NO_VIEW_INDEX) {
            sWorkStringBuilder.append(KEY_SEPARATOR);
            sWorkStringBuilder.append(viewIndex);
          }
        }
      }
    }
    return sWorkStringBuilder.toString();
  }
  @Override
  public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
    ListView list = (ListView) parent;
    int count = mAdapter.getCount();

    if (list.isItemChecked(count - 1)) {
      list.setItemChecked(count - 1, false);
      createNewGroup();
      return;
    }

    for (int i = 0; i < count; i++) {
      mAdapter.getItem(i).setChecked(list.isItemChecked(i));
    }

    // First remove the memberships that have been unchecked
    ArrayList<ValuesDelta> entries = mState.getMimeEntries(GroupMembership.CONTENT_ITEM_TYPE);
    if (entries != null) {
      for (ValuesDelta entry : entries) {
        if (!entry.isDelete()) {
          Long groupId = entry.getGroupRowId();
          if (groupId != null
              && groupId != mFavoritesGroupId
              && (groupId != mDefaultGroupId || mDefaultGroupVisible)
              && !isGroupChecked(groupId)) {
            entry.markDeleted();
          }
        }
      }
    }

    // Now add the newly selected items
    for (int i = 0; i < count; i++) {
      GroupSelectionItem item = mAdapter.getItem(i);
      long groupId = item.getGroupId();
      if (item.isChecked() && !hasMembership(groupId)) {
        ValuesDelta entry = RawContactModifier.insertChild(mState, mKind);
        entry.setGroupRowId(groupId);
      }
    }

    updateView();
  }
  /**
   * Merge the "after" values from the given {@link RawContactDelta} onto the "before" state
   * represented by this {@link RawContactDelta}, discarding any existing "after" states. This is
   * typically used when re-parenting changes onto an updated {@link Entity}.
   */
  public static RawContactDelta mergeAfter(RawContactDelta local, RawContactDelta remote) {
    // Bail early if trying to merge delete with missing local
    final ValuesDelta remoteValues = remote.mValues;
    if (local == null && (remoteValues.isDelete() || remoteValues.isTransient())) return null;

    // Create local version if none exists yet
    if (local == null) local = new RawContactDelta();

    if (LOGV) {
      final Long localVersion =
          (local.mValues == null) ? null : local.mValues.getAsLong(RawContacts.VERSION);
      final Long remoteVersion = remote.mValues.getAsLong(RawContacts.VERSION);
      Log.d(TAG, "Re-parenting from original version " + remoteVersion + " to " + localVersion);
    }

    // Create values if needed, and merge "after" changes
    local.mValues = ValuesDelta.mergeAfter(local.mValues, remote.mValues);

    // Find matching local entry for each remote values, or create
    for (ArrayList<ValuesDelta> mimeEntries : remote.mEntries.values()) {
      for (ValuesDelta remoteEntry : mimeEntries) {
        final Long childId = remoteEntry.getId();

        // Find or create local match and merge
        final ValuesDelta localEntry = local.getEntry(childId);
        final ValuesDelta merged = ValuesDelta.mergeAfter(localEntry, remoteEntry);

        if (localEntry == null && merged != null) {
          // No local entry before, so insert
          local.addEntry(merged);
        }
      }
    }

    return local;
  }
  @Override
  public void setValues(
      DataKind kind,
      ValuesDelta entry,
      RawContactDelta state,
      boolean readOnly,
      ViewIdGenerator vig) {
    super.setValues(kind, entry, state, readOnly, vig);
    // Remove edit texts that we currently have
    if (mFieldEditTexts != null) {
      for (EditText fieldEditText : mFieldEditTexts) {
        mFields.removeView(fieldEditText);
      }
    }
    boolean hidePossible = false;

    int fieldCount = kind.fieldList.size();
    mFieldEditTexts = new EditText[fieldCount];
    for (int index = 0; index < fieldCount; index++) {
      final EditField field = kind.fieldList.get(index);
      final EditText fieldView = new EditText(mContext);
      fieldView.setLayoutParams(
          new LinearLayout.LayoutParams(
              LayoutParams.MATCH_PARENT,
              field.isMultiLine() ? LayoutParams.WRAP_CONTENT : mMinFieldHeight));
      // Set either a minimum line requirement or a minimum height (because {@link TextView}
      // only takes one or the other at a single time).
      if (field.minLines != 0) {
        fieldView.setMinLines(field.minLines);
      } else {
        fieldView.setMinHeight(mMinFieldHeight);
      }
      fieldView.setTextAppearance(getContext(), android.R.style.TextAppearance_Medium);
      fieldView.setGravity(Gravity.TOP);
      mFieldEditTexts[index] = fieldView;
      fieldView.setId(vig.getId(state, kind, entry, index));
      if (field.titleRes > 0) {
        fieldView.setHint(field.titleRes);
      }
      int inputType = field.inputType;
      fieldView.setInputType(inputType);
      if (inputType == InputType.TYPE_CLASS_PHONE) {
        PhoneNumberFormatter.setPhoneNumberFormattingTextWatcher(mContext, fieldView);
        fieldView.setTextDirection(View.TEXT_DIRECTION_LTR);
      }

      // Show the "next" button in IME to navigate between text fields
      // TODO: Still need to properly navigate to/from sections without text fields,
      // See Bug: 5713510
      fieldView.setImeOptions(EditorInfo.IME_ACTION_NEXT);

      // Read current value from state
      final String column = field.column;
      final String value = entry.getAsString(column);
      fieldView.setText(value);

      // Show the delete button if we have a non-null value
      setDeleteButtonVisible(value != null);

      // Prepare listener for writing changes
      fieldView.addTextChangedListener(
          new TextWatcher() {
            @Override
            public void afterTextChanged(Editable s) {
              // Trigger event for newly changed value
              onFieldChanged(column, s.toString());
            }

            @Override
            public void beforeTextChanged(CharSequence s, int start, int count, int after) {}

            @Override
            public void onTextChanged(CharSequence s, int start, int before, int count) {}
          });

      fieldView.setEnabled(isEnabled() && !readOnly);

      if (field.shortForm) {
        hidePossible = true;
        mHasShortAndLongForms = true;
        fieldView.setVisibility(mHideOptional ? View.VISIBLE : View.GONE);
      } else if (field.longForm) {
        hidePossible = true;
        mHasShortAndLongForms = true;
        fieldView.setVisibility(mHideOptional ? View.GONE : View.VISIBLE);
      } else {
        // Hide field when empty and optional value
        final boolean couldHide = (!ContactsUtils.isGraphic(value) && field.optional);
        final boolean willHide = (mHideOptional && couldHide);
        fieldView.setVisibility(willHide ? View.GONE : View.VISIBLE);
        hidePossible = hidePossible || couldHide;
      }

      mFields.addView(fieldView);
    }

    // When hiding fields, place expandable
    setupExpansionView(hidePossible, mHideOptional);
    mExpansionView.setEnabled(!readOnly && isEnabled());
  }
 protected void saveValue(String column, String value) {
   mEntry.put(column, value);
 }
 public boolean isContactInsert() {
   return mValues.isInsert();
 }
 public ValuesDelta addEntry(ValuesDelta entry) {
   final String mimeType = entry.getMimetype();
   getMimeEntries(mimeType, true).add(entry);
   return entry;
 }
  /**
   * For compatibility purpose, this method is copied from {@link #buildDiff} and takes an ArrayList
   * of CPOWrapper as parameter.
   */
  public void buildDiffWrapper(ArrayList<CPOWrapper> buildInto) {
    final int firstIndex = buildInto.size();

    final boolean isContactInsert = mValues.isInsert();
    final boolean isContactDelete = mValues.isDelete();
    final boolean isContactUpdate = !isContactInsert && !isContactDelete;

    final Long beforeId = mValues.getId();

    if (isContactInsert) {
      // TODO: for now simply disabling aggregation when a new contact is
      // created on the phone.  In the future, will show aggregation suggestions
      // after saving the contact.
      mValues.put(RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE_SUSPENDED);
    }

    // Build possible operation at Contact level
    BuilderWrapper bw = mValues.buildDiffWrapper(mContactsQueryUri);
    possibleAddWrapper(buildInto, bw);

    // Build operations for all children
    for (ArrayList<ValuesDelta> mimeEntries : mEntries.values()) {
      for (ValuesDelta child : mimeEntries) {
        // Ignore children if parent was deleted
        if (isContactDelete) continue;

        // Use the profile data URI if the contact is the profile.
        if (mContactsQueryUri.equals(Profile.CONTENT_RAW_CONTACTS_URI)) {
          bw =
              child.buildDiffWrapper(
                  Uri.withAppendedPath(Profile.CONTENT_URI, RawContacts.Data.CONTENT_DIRECTORY));
        } else {
          bw = child.buildDiffWrapper(Data.CONTENT_URI);
        }

        if (child.isInsert()) {
          if (isContactInsert) {
            // Parent is brand new insert, so back-reference _id
            bw.getBuilder().withValueBackReference(Data.RAW_CONTACT_ID, firstIndex);
          } else {
            // Inserting under existing, so fill with known _id
            bw.getBuilder().withValue(Data.RAW_CONTACT_ID, beforeId);
          }
        } else if (isContactInsert && bw != null && bw.getBuilder() != null) {
          // Child must be insert when Contact insert
          throw new IllegalArgumentException("When parent insert, child must be also");
        }
        possibleAddWrapper(buildInto, bw);
      }
    }

    final boolean addedOperations = buildInto.size() > firstIndex;
    if (addedOperations && isContactUpdate) {
      // Suspend aggregation while persisting updates
      Builder builder = buildSetAggregationMode(beforeId, RawContacts.AGGREGATION_MODE_SUSPENDED);
      buildInto.add(firstIndex, new CPOWrapper(builder.build(), CompatUtils.TYPE_UPDATE));

      // Restore aggregation mode as last operation
      builder = buildSetAggregationMode(beforeId, RawContacts.AGGREGATION_MODE_DEFAULT);
      buildInto.add(new CPOWrapper(builder.build(), CompatUtils.TYPE_UPDATE));
    } else if (isContactInsert) {
      // Restore aggregation mode as last operation
      Builder builder = ContentProviderOperation.newUpdate(mContactsQueryUri);
      builder.withValue(RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE_DEFAULT);
      builder.withSelection(RawContacts._ID + "=?", new String[1]);
      builder.withSelectionBackReference(0, firstIndex);
      buildInto.add(new CPOWrapper(builder.build(), CompatUtils.TYPE_UPDATE));
    }
  }