/** 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)); } }