| /* |
| * Copyright (C) 2009 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.contacts.model; |
| |
| import com.android.contacts.ContactsUtils; |
| import com.android.contacts.model.ContactsSource.DataKind; |
| import com.android.contacts.model.ContactsSource.EditField; |
| import com.android.contacts.model.ContactsSource.EditType; |
| import com.android.contacts.model.EntityDelta.ValuesDelta; |
| import com.google.android.collect.Lists; |
| |
| import android.content.ContentValues; |
| import android.content.Context; |
| import android.database.Cursor; |
| import android.os.Bundle; |
| import android.provider.ContactsContract.Data; |
| import android.provider.ContactsContract.Intents; |
| import android.provider.ContactsContract.RawContacts; |
| import android.provider.ContactsContract.CommonDataKinds.BaseTypes; |
| import android.provider.ContactsContract.CommonDataKinds.Email; |
| import android.provider.ContactsContract.CommonDataKinds.Im; |
| import android.provider.ContactsContract.CommonDataKinds.Phone; |
| import android.provider.ContactsContract.CommonDataKinds.Photo; |
| import android.provider.ContactsContract.CommonDataKinds.StructuredName; |
| import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; |
| import android.provider.ContactsContract.Intents.Insert; |
| import android.provider.ContactsContract; |
| import android.text.TextUtils; |
| import android.util.Log; |
| import android.util.SparseIntArray; |
| |
| import java.util.ArrayList; |
| import java.util.Iterator; |
| import java.util.List; |
| |
| /** |
| * Helper methods for modifying an {@link EntityDelta}, such as inserting |
| * new rows, or enforcing {@link ContactsSource}. |
| */ |
| public class EntityModifier { |
| private static final String TAG = "EntityModifier"; |
| |
| /** |
| * For the given {@link EntityDelta}, determine if the given |
| * {@link DataKind} could be inserted under specific |
| * {@link ContactsSource}. |
| */ |
| public static boolean canInsert(EntityDelta state, DataKind kind) { |
| // Insert possible when have valid types and under overall maximum |
| final int visibleCount = state.getMimeEntriesCount(kind.mimeType, true); |
| final boolean validTypes = hasValidTypes(state, kind); |
| final boolean validOverall = (kind.typeOverallMax == -1) |
| || (visibleCount < kind.typeOverallMax); |
| return (validTypes && validOverall); |
| } |
| |
| public static boolean hasValidTypes(EntityDelta state, DataKind kind) { |
| if (EntityModifier.hasEditTypes(kind)) { |
| return (getValidTypes(state, kind).size() > 0); |
| } else { |
| return true; |
| } |
| } |
| |
| /** |
| * Ensure that at least one of the given {@link DataKind} exists in the |
| * given {@link EntityDelta} state, and try creating one if none exist. |
| */ |
| public static void ensureKindExists(EntityDelta state, ContactsSource source, String mimeType) { |
| final DataKind kind = source.getKindForMimetype(mimeType); |
| final boolean hasChild = state.getMimeEntriesCount(mimeType, true) > 0; |
| |
| if (!hasChild && kind != null) { |
| // Create child when none exists and valid kind |
| final ValuesDelta child = insertChild(state, kind); |
| if (kind.mimeType.equals(Photo.CONTENT_ITEM_TYPE)) { |
| child.setFromTemplate(true); |
| } |
| } |
| } |
| |
| /** |
| * For the given {@link EntityDelta} and {@link DataKind}, return the |
| * list possible {@link EditType} options available based on |
| * {@link ContactsSource}. |
| */ |
| public static ArrayList<EditType> getValidTypes(EntityDelta state, DataKind kind) { |
| return getValidTypes(state, kind, null, true, null); |
| } |
| |
| /** |
| * For the given {@link EntityDelta} and {@link DataKind}, return the |
| * list possible {@link EditType} options available based on |
| * {@link ContactsSource}. |
| * |
| * @param forceInclude Always include this {@link EditType} in the returned |
| * list, even when an otherwise-invalid choice. This is useful |
| * when showing a dialog that includes the current type. |
| */ |
| public static ArrayList<EditType> getValidTypes(EntityDelta state, DataKind kind, |
| EditType forceInclude) { |
| return getValidTypes(state, kind, forceInclude, true, null); |
| } |
| |
| /** |
| * For the given {@link EntityDelta} and {@link DataKind}, return the |
| * list possible {@link EditType} options available based on |
| * {@link ContactsSource}. |
| * |
| * @param forceInclude Always include this {@link EditType} in the returned |
| * list, even when an otherwise-invalid choice. This is useful |
| * when showing a dialog that includes the current type. |
| * @param includeSecondary If true, include any valid types marked as |
| * {@link EditType#secondary}. |
| * @param typeCount When provided, will be used for the frequency count of |
| * each {@link EditType}, otherwise built using |
| * {@link #getTypeFrequencies(EntityDelta, DataKind)}. |
| */ |
| private static ArrayList<EditType> getValidTypes(EntityDelta state, DataKind kind, |
| EditType forceInclude, boolean includeSecondary, SparseIntArray typeCount) { |
| final ArrayList<EditType> validTypes = Lists.newArrayList(); |
| |
| // Bail early if no types provided |
| if (!hasEditTypes(kind)) return validTypes; |
| |
| if (typeCount == null) { |
| // Build frequency counts if not provided |
| typeCount = getTypeFrequencies(state, kind); |
| } |
| |
| // Build list of valid types |
| final int overallCount = typeCount.get(FREQUENCY_TOTAL); |
| for (EditType type : kind.typeList) { |
| final boolean validOverall = (kind.typeOverallMax == -1 ? true |
| : overallCount < kind.typeOverallMax); |
| final boolean validSpecific = (type.specificMax == -1 ? true : typeCount |
| .get(type.rawValue) < type.specificMax); |
| final boolean validSecondary = (includeSecondary ? true : !type.secondary); |
| final boolean forcedInclude = type.equals(forceInclude); |
| if (forcedInclude || (validOverall && validSpecific && validSecondary)) { |
| // Type is valid when no limit, under limit, or forced include |
| validTypes.add(type); |
| } |
| } |
| |
| return validTypes; |
| } |
| |
| private static final int FREQUENCY_TOTAL = Integer.MIN_VALUE; |
| |
| /** |
| * Count up the frequency that each {@link EditType} appears in the given |
| * {@link EntityDelta}. The returned {@link SparseIntArray} maps from |
| * {@link EditType#rawValue} to counts, with the total overall count stored |
| * as {@link #FREQUENCY_TOTAL}. |
| */ |
| private static SparseIntArray getTypeFrequencies(EntityDelta state, DataKind kind) { |
| final SparseIntArray typeCount = new SparseIntArray(); |
| |
| // Find all entries for this kind, bailing early if none found |
| final List<ValuesDelta> mimeEntries = state.getMimeEntries(kind.mimeType); |
| if (mimeEntries == null) return typeCount; |
| |
| int totalCount = 0; |
| for (ValuesDelta entry : mimeEntries) { |
| // Only count visible entries |
| if (!entry.isVisible()) continue; |
| totalCount++; |
| |
| final EditType type = getCurrentType(entry, kind); |
| if (type != null) { |
| final int count = typeCount.get(type.rawValue); |
| typeCount.put(type.rawValue, count + 1); |
| } |
| } |
| typeCount.put(FREQUENCY_TOTAL, totalCount); |
| return typeCount; |
| } |
| |
| /** |
| * Check if the given {@link DataKind} has multiple types that should be |
| * displayed for users to pick. |
| */ |
| public static boolean hasEditTypes(DataKind kind) { |
| return kind.typeList != null && kind.typeList.size() > 0; |
| } |
| |
| /** |
| * Find the {@link EditType} that describes the given |
| * {@link ValuesDelta} row, assuming the given {@link DataKind} dictates |
| * the possible types. |
| */ |
| public static EditType getCurrentType(ValuesDelta entry, DataKind kind) { |
| final Long rawValue = entry.getAsLong(kind.typeColumn); |
| if (rawValue == null) return null; |
| return getType(kind, rawValue.intValue()); |
| } |
| |
| /** |
| * Find the {@link EditType} that describes the given {@link ContentValues} row, |
| * assuming the given {@link DataKind} dictates the possible types. |
| */ |
| public static EditType getCurrentType(ContentValues entry, DataKind kind) { |
| if (kind.typeColumn == null) return null; |
| final Integer rawValue = entry.getAsInteger(kind.typeColumn); |
| if (rawValue == null) return null; |
| return getType(kind, rawValue); |
| } |
| |
| /** |
| * Find the {@link EditType} that describes the given {@link Cursor} row, |
| * assuming the given {@link DataKind} dictates the possible types. |
| */ |
| public static EditType getCurrentType(Cursor cursor, DataKind kind) { |
| if (kind.typeColumn == null) return null; |
| final int index = cursor.getColumnIndex(kind.typeColumn); |
| if (index == -1) return null; |
| final int rawValue = cursor.getInt(index); |
| return getType(kind, rawValue); |
| } |
| |
| /** |
| * Find the {@link EditType} with the given {@link EditType#rawValue}. |
| */ |
| public static EditType getType(DataKind kind, int rawValue) { |
| for (EditType type : kind.typeList) { |
| if (type.rawValue == rawValue) { |
| return type; |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * Return the precedence for the the given {@link EditType#rawValue}, where |
| * lower numbers are higher precedence. |
| */ |
| public static int getTypePrecedence(DataKind kind, int rawValue) { |
| for (int i = 0; i < kind.typeList.size(); i++) { |
| final EditType type = kind.typeList.get(i); |
| if (type.rawValue == rawValue) { |
| return i; |
| } |
| } |
| return Integer.MAX_VALUE; |
| } |
| |
| /** |
| * Find the best {@link EditType} for a potential insert. The "best" is the |
| * first primary type that doesn't already exist. When all valid types |
| * exist, we pick the last valid option. |
| */ |
| public static EditType getBestValidType(EntityDelta state, DataKind kind, |
| boolean includeSecondary, int exactValue) { |
| // Shortcut when no types |
| if (kind.typeColumn == null) return null; |
| |
| // Find type counts and valid primary types, bail if none |
| final SparseIntArray typeCount = getTypeFrequencies(state, kind); |
| final ArrayList<EditType> validTypes = getValidTypes(state, kind, null, includeSecondary, |
| typeCount); |
| if (validTypes.size() == 0) return null; |
| |
| // Keep track of the last valid type |
| final EditType lastType = validTypes.get(validTypes.size() - 1); |
| |
| // Remove any types that already exist |
| Iterator<EditType> iterator = validTypes.iterator(); |
| while (iterator.hasNext()) { |
| final EditType type = iterator.next(); |
| final int count = typeCount.get(type.rawValue); |
| |
| if (exactValue == type.rawValue) { |
| // Found exact value match |
| return type; |
| } |
| |
| if (count > 0) { |
| // Type already appears, so don't consider |
| iterator.remove(); |
| } |
| } |
| |
| // Use the best remaining, otherwise the last valid |
| if (validTypes.size() > 0) { |
| return validTypes.get(0); |
| } else { |
| return lastType; |
| } |
| } |
| |
| /** |
| * Insert a new child of kind {@link DataKind} into the given |
| * {@link EntityDelta}. Tries using the best {@link EditType} found using |
| * {@link #getBestValidType(EntityDelta, DataKind, boolean, int)}. |
| */ |
| public static ValuesDelta insertChild(EntityDelta state, DataKind kind) { |
| // First try finding a valid primary |
| EditType bestType = getBestValidType(state, kind, false, Integer.MIN_VALUE); |
| if (bestType == null) { |
| // No valid primary found, so expand search to secondary |
| bestType = getBestValidType(state, kind, true, Integer.MIN_VALUE); |
| } |
| return insertChild(state, kind, bestType); |
| } |
| |
| /** |
| * Insert a new child of kind {@link DataKind} into the given |
| * {@link EntityDelta}, marked with the given {@link EditType}. |
| */ |
| public static ValuesDelta insertChild(EntityDelta state, DataKind kind, EditType type) { |
| // Bail early if invalid kind |
| if (kind == null) return null; |
| final ContentValues after = new ContentValues(); |
| |
| // Our parent CONTACT_ID is provided later |
| after.put(Data.MIMETYPE, kind.mimeType); |
| |
| // Fill-in with any requested default values |
| if (kind.defaultValues != null) { |
| after.putAll(kind.defaultValues); |
| } |
| |
| if (kind.typeColumn != null && type != null) { |
| // Set type, if provided |
| after.put(kind.typeColumn, type.rawValue); |
| } |
| |
| final ValuesDelta child = ValuesDelta.fromAfter(after); |
| state.addEntry(child); |
| return child; |
| } |
| |
| /** |
| * Processing to trim any empty {@link ValuesDelta} and {@link EntityDelta} |
| * from the given {@link EntitySet}, assuming the given {@link Sources} |
| * dictates the structure for various fields. This method ignores rows not |
| * described by the {@link ContactsSource}. |
| */ |
| public static void trimEmpty(EntitySet set, Sources sources) { |
| for (EntityDelta state : set) { |
| final String accountType = state.getValues().getAsString(RawContacts.ACCOUNT_TYPE); |
| final ContactsSource source = sources.getInflatedSource(accountType, |
| ContactsSource.LEVEL_MIMETYPES); |
| trimEmpty(state, source); |
| } |
| } |
| |
| /** |
| * Processing to trim any empty {@link ValuesDelta} rows from the given |
| * {@link EntityDelta}, assuming the given {@link ContactsSource} dictates |
| * the structure for various fields. This method ignores rows not described |
| * by the {@link ContactsSource}. |
| */ |
| public static void trimEmpty(EntityDelta state, ContactsSource source) { |
| boolean hasValues = false; |
| |
| // Walk through entries for each well-known kind |
| for (DataKind kind : source.getSortedDataKinds()) { |
| final String mimeType = kind.mimeType; |
| final ArrayList<ValuesDelta> entries = state.getMimeEntries(mimeType); |
| if (entries == null) continue; |
| |
| for (ValuesDelta entry : entries) { |
| // Skip any values that haven't been touched |
| final boolean touched = entry.isInsert() || entry.isUpdate(); |
| if (!touched) { |
| hasValues = true; |
| continue; |
| } |
| |
| // Test and remove this row if empty and it isn't a photo from google |
| final boolean isGoogleSource = TextUtils.equals(GoogleSource.ACCOUNT_TYPE, |
| state.getValues().getAsString(RawContacts.ACCOUNT_TYPE)); |
| final boolean isPhoto = TextUtils.equals(Photo.CONTENT_ITEM_TYPE, kind.mimeType); |
| final boolean isGooglePhoto = isPhoto && isGoogleSource; |
| |
| if (EntityModifier.isEmpty(entry, kind) && !isGooglePhoto) { |
| // TODO: remove this verbose logging |
| Log.w(TAG, "Trimming: " + entry.toString()); |
| entry.markDeleted(); |
| } else if (!entry.isFromTemplate()) { |
| hasValues = true; |
| } |
| } |
| } |
| if (!hasValues) { |
| // Trim overall entity if no children exist |
| state.markDeleted(); |
| } |
| } |
| |
| /** |
| * Test if the given {@link ValuesDelta} would be considered "empty" in |
| * terms of {@link DataKind#fieldList}. |
| */ |
| public static boolean isEmpty(ValuesDelta values, DataKind kind) { |
| // No defined fields mean this row is always empty |
| if (kind.fieldList == null) return true; |
| |
| boolean hasValues = false; |
| for (EditField field : kind.fieldList) { |
| // If any field has values, we're not empty |
| final String value = values.getAsString(field.column); |
| if (ContactsUtils.isGraphic(value)) { |
| hasValues = true; |
| } |
| } |
| |
| return !hasValues; |
| } |
| |
| /** |
| * Parse the given {@link Bundle} into the given {@link EntityDelta} state, |
| * assuming the extras defined through {@link Intents}. |
| */ |
| public static void parseExtras(Context context, ContactsSource source, EntityDelta state, |
| Bundle extras) { |
| if (extras == null || extras.size() == 0) { |
| // Bail early if no useful data |
| return; |
| } |
| |
| { |
| // StructuredName |
| EntityModifier.ensureKindExists(state, source, StructuredName.CONTENT_ITEM_TYPE); |
| final ValuesDelta child = state.getPrimaryEntry(StructuredName.CONTENT_ITEM_TYPE); |
| |
| final String name = extras.getString(Insert.NAME); |
| if (ContactsUtils.isGraphic(name)) { |
| child.put(StructuredName.GIVEN_NAME, name); |
| } |
| |
| final String phoneticName = extras.getString(Insert.PHONETIC_NAME); |
| if (ContactsUtils.isGraphic(phoneticName)) { |
| child.put(StructuredName.PHONETIC_GIVEN_NAME, phoneticName); |
| } |
| } |
| |
| { |
| // StructuredPostal |
| final DataKind kind = source.getKindForMimetype(StructuredPostal.CONTENT_ITEM_TYPE); |
| parseExtras(state, kind, extras, Insert.POSTAL_TYPE, Insert.POSTAL, |
| StructuredPostal.STREET); |
| } |
| |
| { |
| // Phone |
| final DataKind kind = source.getKindForMimetype(Phone.CONTENT_ITEM_TYPE); |
| parseExtras(state, kind, extras, Insert.PHONE_TYPE, Insert.PHONE, Phone.NUMBER); |
| parseExtras(state, kind, extras, Insert.SECONDARY_PHONE_TYPE, Insert.SECONDARY_PHONE, |
| Phone.NUMBER); |
| parseExtras(state, kind, extras, Insert.TERTIARY_PHONE_TYPE, Insert.TERTIARY_PHONE, |
| Phone.NUMBER); |
| } |
| |
| { |
| // Email |
| final DataKind kind = source.getKindForMimetype(Email.CONTENT_ITEM_TYPE); |
| parseExtras(state, kind, extras, Insert.EMAIL_TYPE, Insert.EMAIL, Email.DATA); |
| parseExtras(state, kind, extras, Insert.SECONDARY_EMAIL_TYPE, Insert.SECONDARY_EMAIL, |
| Email.DATA); |
| parseExtras(state, kind, extras, Insert.TERTIARY_EMAIL_TYPE, Insert.TERTIARY_EMAIL, |
| Email.DATA); |
| } |
| |
| { |
| // Im |
| final DataKind kind = source.getKindForMimetype(Im.CONTENT_ITEM_TYPE); |
| fixupLegacyImType(extras); |
| parseExtras(state, kind, extras, Insert.IM_PROTOCOL, Insert.IM_HANDLE, Im.DATA); |
| } |
| } |
| |
| /** |
| * Attempt to parse legacy {@link Insert#IM_PROTOCOL} values, replacing them |
| * with updated values. |
| */ |
| private static void fixupLegacyImType(Bundle bundle) { |
| final String encodedString = bundle.getString(Insert.IM_PROTOCOL); |
| if (encodedString == null) return; |
| |
| try { |
| final Object protocol = android.provider.Contacts.ContactMethods |
| .decodeImProtocol(encodedString); |
| if (protocol instanceof Integer) { |
| bundle.putInt(Insert.IM_PROTOCOL, (Integer)protocol); |
| } else { |
| bundle.putString(Insert.IM_PROTOCOL, (String)protocol); |
| } |
| } catch (IllegalArgumentException e) { |
| // Ignore exception when legacy parser fails |
| } |
| } |
| |
| /** |
| * Parse a specific entry from the given {@link Bundle} and insert into the |
| * given {@link EntityDelta}. Silently skips the insert when missing value |
| * or no valid {@link EditType} found. |
| * |
| * @param typeExtra {@link Bundle} key that holds the incoming |
| * {@link EditType#rawValue} value. |
| * @param valueExtra {@link Bundle} key that holds the incoming value. |
| * @param valueColumn Column to write value into {@link ValuesDelta}. |
| */ |
| public static void parseExtras(EntityDelta state, DataKind kind, Bundle extras, |
| String typeExtra, String valueExtra, String valueColumn) { |
| final CharSequence value = extras.getCharSequence(valueExtra); |
| |
| // Bail early if source doesn't handle this type |
| if (kind == null) return; |
| |
| // Bail when can't insert type, or value missing |
| final boolean canInsert = EntityModifier.canInsert(state, kind); |
| final boolean validValue = (value != null && TextUtils.isGraphic(value)); |
| if (!validValue || !canInsert) return; |
| |
| // Find exact type when requested, otherwise best available type |
| final boolean hasType = extras.containsKey(typeExtra); |
| final int typeValue = extras.getInt(typeExtra, hasType ? BaseTypes.TYPE_CUSTOM |
| : Integer.MIN_VALUE); |
| final EditType editType = EntityModifier.getBestValidType(state, kind, true, typeValue); |
| |
| // Create data row and fill with value |
| final ValuesDelta child = EntityModifier.insertChild(state, kind, editType); |
| child.put(valueColumn, value.toString()); |
| |
| if (editType != null && editType.customColumn != null) { |
| // Write down label when custom type picked |
| final String customType = extras.getString(typeExtra); |
| child.put(editType.customColumn, customType); |
| } |
| } |
| } |