Home | History | Annotate | Download | only in model
      1 /*
      2  * Copyright (C) 2009 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.common.model;
     18 
     19 import android.content.ContentValues;
     20 import android.content.Context;
     21 import android.database.Cursor;
     22 import android.net.Uri;
     23 import android.os.Bundle;
     24 import android.provider.ContactsContract;
     25 import android.provider.ContactsContract.CommonDataKinds.BaseTypes;
     26 import android.provider.ContactsContract.CommonDataKinds.Email;
     27 import android.provider.ContactsContract.CommonDataKinds.Event;
     28 import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
     29 import android.provider.ContactsContract.CommonDataKinds.Im;
     30 import android.provider.ContactsContract.CommonDataKinds.Nickname;
     31 import android.provider.ContactsContract.CommonDataKinds.Note;
     32 import android.provider.ContactsContract.CommonDataKinds.Organization;
     33 import android.provider.ContactsContract.CommonDataKinds.Phone;
     34 import android.provider.ContactsContract.CommonDataKinds.Photo;
     35 import android.provider.ContactsContract.CommonDataKinds.Relation;
     36 import android.provider.ContactsContract.CommonDataKinds.SipAddress;
     37 import android.provider.ContactsContract.CommonDataKinds.StructuredName;
     38 import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
     39 import android.provider.ContactsContract.CommonDataKinds.Website;
     40 import android.provider.ContactsContract.Data;
     41 import android.provider.ContactsContract.Intents;
     42 import android.provider.ContactsContract.Intents.Insert;
     43 import android.provider.ContactsContract.RawContacts;
     44 import android.text.TextUtils;
     45 import android.util.Log;
     46 import android.util.SparseArray;
     47 import android.util.SparseIntArray;
     48 
     49 import com.android.contacts.common.ContactsUtils;
     50 import com.android.contacts.common.model.AccountTypeManager;
     51 import com.android.contacts.common.model.ValuesDelta;
     52 import com.android.contacts.common.util.CommonDateUtils;
     53 import com.android.contacts.common.util.DateUtils;
     54 import com.android.contacts.common.util.NameConverter;
     55 import com.android.contacts.common.model.account.AccountType;
     56 import com.android.contacts.common.model.account.AccountType.EditField;
     57 import com.android.contacts.common.model.account.AccountType.EditType;
     58 import com.android.contacts.common.model.account.AccountType.EventEditType;
     59 import com.android.contacts.common.model.account.GoogleAccountType;
     60 import com.android.contacts.common.model.dataitem.DataKind;
     61 import com.android.contacts.common.model.dataitem.PhoneDataItem;
     62 import com.android.contacts.common.model.dataitem.StructuredNameDataItem;
     63 
     64 import java.text.ParsePosition;
     65 import java.util.ArrayList;
     66 import java.util.Arrays;
     67 import java.util.Calendar;
     68 import java.util.Date;
     69 import java.util.HashSet;
     70 import java.util.Iterator;
     71 import java.util.List;
     72 import java.util.Locale;
     73 import java.util.Set;
     74 
     75 /**
     76  * Helper methods for modifying an {@link RawContactDelta}, such as inserting
     77  * new rows, or enforcing {@link AccountType}.
     78  */
     79 public class RawContactModifier {
     80     private static final String TAG = RawContactModifier.class.getSimpleName();
     81 
     82     /** Set to true in order to view logs on entity operations */
     83     private static final boolean DEBUG = false;
     84 
     85     /**
     86      * For the given {@link RawContactDelta}, determine if the given
     87      * {@link DataKind} could be inserted under specific
     88      * {@link AccountType}.
     89      */
     90     public static boolean canInsert(RawContactDelta state, DataKind kind) {
     91         // Insert possible when have valid types and under overall maximum
     92         final int visibleCount = state.getMimeEntriesCount(kind.mimeType, true);
     93         final boolean validTypes = hasValidTypes(state, kind);
     94         final boolean validOverall = (kind.typeOverallMax == -1)
     95                 || (visibleCount < kind.typeOverallMax);
     96         return (validTypes && validOverall);
     97     }
     98 
     99     public static boolean hasValidTypes(RawContactDelta state, DataKind kind) {
    100         if (RawContactModifier.hasEditTypes(kind)) {
    101             return (getValidTypes(state, kind).size() > 0);
    102         } else {
    103             return true;
    104         }
    105     }
    106 
    107     /**
    108      * Ensure that at least one of the given {@link DataKind} exists in the
    109      * given {@link RawContactDelta} state, and try creating one if none exist.
    110      * @return The child (either newly created or the first existing one), or null if the
    111      *     account doesn't support this {@link DataKind}.
    112      */
    113     public static ValuesDelta ensureKindExists(
    114             RawContactDelta state, AccountType accountType, String mimeType) {
    115         final DataKind kind = accountType.getKindForMimetype(mimeType);
    116         final boolean hasChild = state.getMimeEntriesCount(mimeType, true) > 0;
    117 
    118         if (kind != null) {
    119             if (hasChild) {
    120                 // Return the first entry.
    121                 return state.getMimeEntries(mimeType).get(0);
    122             } else {
    123                 // Create child when none exists and valid kind
    124                 final ValuesDelta child = insertChild(state, kind);
    125                 if (kind.mimeType.equals(Photo.CONTENT_ITEM_TYPE)) {
    126                     child.setFromTemplate(true);
    127                 }
    128                 return child;
    129             }
    130         }
    131         return null;
    132     }
    133 
    134     /**
    135      * For the given {@link RawContactDelta} and {@link DataKind}, return the
    136      * list possible {@link EditType} options available based on
    137      * {@link AccountType}.
    138      */
    139     public static ArrayList<EditType> getValidTypes(RawContactDelta state, DataKind kind) {
    140         return getValidTypes(state, kind, null, true, null);
    141     }
    142 
    143     /**
    144      * For the given {@link RawContactDelta} and {@link DataKind}, return the
    145      * list possible {@link EditType} options available based on
    146      * {@link AccountType}.
    147      *
    148      * @param forceInclude Always include this {@link EditType} in the returned
    149      *            list, even when an otherwise-invalid choice. This is useful
    150      *            when showing a dialog that includes the current type.
    151      */
    152     public static ArrayList<EditType> getValidTypes(RawContactDelta state, DataKind kind,
    153             EditType forceInclude) {
    154         return getValidTypes(state, kind, forceInclude, true, null);
    155     }
    156 
    157     /**
    158      * For the given {@link RawContactDelta} and {@link DataKind}, return the
    159      * list possible {@link EditType} options available based on
    160      * {@link AccountType}.
    161      *
    162      * @param forceInclude Always include this {@link EditType} in the returned
    163      *            list, even when an otherwise-invalid choice. This is useful
    164      *            when showing a dialog that includes the current type.
    165      * @param includeSecondary If true, include any valid types marked as
    166      *            {@link EditType#secondary}.
    167      * @param typeCount When provided, will be used for the frequency count of
    168      *            each {@link EditType}, otherwise built using
    169      *            {@link #getTypeFrequencies(RawContactDelta, DataKind)}.
    170      */
    171     private static ArrayList<EditType> getValidTypes(RawContactDelta state, DataKind kind,
    172             EditType forceInclude, boolean includeSecondary, SparseIntArray typeCount) {
    173         final ArrayList<EditType> validTypes = new ArrayList<EditType>();
    174 
    175         // Bail early if no types provided
    176         if (!hasEditTypes(kind)) return validTypes;
    177 
    178         if (typeCount == null) {
    179             // Build frequency counts if not provided
    180             typeCount = getTypeFrequencies(state, kind);
    181         }
    182 
    183         // Build list of valid types
    184         final int overallCount = typeCount.get(FREQUENCY_TOTAL);
    185         for (EditType type : kind.typeList) {
    186             final boolean validOverall = (kind.typeOverallMax == -1 ? true
    187                     : overallCount < kind.typeOverallMax);
    188             final boolean validSpecific = (type.specificMax == -1 ? true : typeCount
    189                     .get(type.rawValue) < type.specificMax);
    190             final boolean validSecondary = (includeSecondary ? true : !type.secondary);
    191             final boolean forcedInclude = type.equals(forceInclude);
    192             if (forcedInclude || (validOverall && validSpecific && validSecondary)) {
    193                 // Type is valid when no limit, under limit, or forced include
    194                 validTypes.add(type);
    195             }
    196         }
    197 
    198         return validTypes;
    199     }
    200 
    201     private static final int FREQUENCY_TOTAL = Integer.MIN_VALUE;
    202 
    203     /**
    204      * Count up the frequency that each {@link EditType} appears in the given
    205      * {@link RawContactDelta}. The returned {@link SparseIntArray} maps from
    206      * {@link EditType#rawValue} to counts, with the total overall count stored
    207      * as {@link #FREQUENCY_TOTAL}.
    208      */
    209     private static SparseIntArray getTypeFrequencies(RawContactDelta state, DataKind kind) {
    210         final SparseIntArray typeCount = new SparseIntArray();
    211 
    212         // Find all entries for this kind, bailing early if none found
    213         final List<ValuesDelta> mimeEntries = state.getMimeEntries(kind.mimeType);
    214         if (mimeEntries == null) return typeCount;
    215 
    216         int totalCount = 0;
    217         for (ValuesDelta entry : mimeEntries) {
    218             // Only count visible entries
    219             if (!entry.isVisible()) continue;
    220             totalCount++;
    221 
    222             final EditType type = getCurrentType(entry, kind);
    223             if (type != null) {
    224                 final int count = typeCount.get(type.rawValue);
    225                 typeCount.put(type.rawValue, count + 1);
    226             }
    227         }
    228         typeCount.put(FREQUENCY_TOTAL, totalCount);
    229         return typeCount;
    230     }
    231 
    232     /**
    233      * Check if the given {@link DataKind} has multiple types that should be
    234      * displayed for users to pick.
    235      */
    236     public static boolean hasEditTypes(DataKind kind) {
    237         return kind.typeList != null && kind.typeList.size() > 0;
    238     }
    239 
    240     /**
    241      * Find the {@link EditType} that describes the given
    242      * {@link ValuesDelta} row, assuming the given {@link DataKind} dictates
    243      * the possible types.
    244      */
    245     public static EditType getCurrentType(ValuesDelta entry, DataKind kind) {
    246         final Long rawValue = entry.getAsLong(kind.typeColumn);
    247         if (rawValue == null) return null;
    248         return getType(kind, rawValue.intValue());
    249     }
    250 
    251     /**
    252      * Find the {@link EditType} that describes the given {@link ContentValues} row,
    253      * assuming the given {@link DataKind} dictates the possible types.
    254      */
    255     public static EditType getCurrentType(ContentValues entry, DataKind kind) {
    256         if (kind.typeColumn == null) return null;
    257         final Integer rawValue = entry.getAsInteger(kind.typeColumn);
    258         if (rawValue == null) return null;
    259         return getType(kind, rawValue);
    260     }
    261 
    262     /**
    263      * Find the {@link EditType} that describes the given {@link Cursor} row,
    264      * assuming the given {@link DataKind} dictates the possible types.
    265      */
    266     public static EditType getCurrentType(Cursor cursor, DataKind kind) {
    267         if (kind.typeColumn == null) return null;
    268         final int index = cursor.getColumnIndex(kind.typeColumn);
    269         if (index == -1) return null;
    270         final int rawValue = cursor.getInt(index);
    271         return getType(kind, rawValue);
    272     }
    273 
    274     /**
    275      * Find the {@link EditType} with the given {@link EditType#rawValue}.
    276      */
    277     public static EditType getType(DataKind kind, int rawValue) {
    278         for (EditType type : kind.typeList) {
    279             if (type.rawValue == rawValue) {
    280                 return type;
    281             }
    282         }
    283         return null;
    284     }
    285 
    286     /**
    287      * Return the precedence for the the given {@link EditType#rawValue}, where
    288      * lower numbers are higher precedence.
    289      */
    290     public static int getTypePrecedence(DataKind kind, int rawValue) {
    291         for (int i = 0; i < kind.typeList.size(); i++) {
    292             final EditType type = kind.typeList.get(i);
    293             if (type.rawValue == rawValue) {
    294                 return i;
    295             }
    296         }
    297         return Integer.MAX_VALUE;
    298     }
    299 
    300     /**
    301      * Find the best {@link EditType} for a potential insert. The "best" is the
    302      * first primary type that doesn't already exist. When all valid types
    303      * exist, we pick the last valid option.
    304      */
    305     public static EditType getBestValidType(RawContactDelta state, DataKind kind,
    306             boolean includeSecondary, int exactValue) {
    307         // Shortcut when no types
    308         if (kind == null || kind.typeColumn == null) return null;
    309 
    310         // Find type counts and valid primary types, bail if none
    311         final SparseIntArray typeCount = getTypeFrequencies(state, kind);
    312         final ArrayList<EditType> validTypes = getValidTypes(state, kind, null, includeSecondary,
    313                 typeCount);
    314         if (validTypes.size() == 0) return null;
    315 
    316         // Keep track of the last valid type
    317         final EditType lastType = validTypes.get(validTypes.size() - 1);
    318 
    319         // Remove any types that already exist
    320         Iterator<EditType> iterator = validTypes.iterator();
    321         while (iterator.hasNext()) {
    322             final EditType type = iterator.next();
    323             final int count = typeCount.get(type.rawValue);
    324 
    325             if (exactValue == type.rawValue) {
    326                 // Found exact value match
    327                 return type;
    328             }
    329 
    330             if (count > 0) {
    331                 // Type already appears, so don't consider
    332                 iterator.remove();
    333             }
    334         }
    335 
    336         // Use the best remaining, otherwise the last valid
    337         if (validTypes.size() > 0) {
    338             return validTypes.get(0);
    339         } else {
    340             return lastType;
    341         }
    342     }
    343 
    344     /**
    345      * Insert a new child of kind {@link DataKind} into the given
    346      * {@link RawContactDelta}. Tries using the best {@link EditType} found using
    347      * {@link #getBestValidType(RawContactDelta, DataKind, boolean, int)}.
    348      */
    349     public static ValuesDelta insertChild(RawContactDelta state, DataKind kind) {
    350         // Bail early if invalid kind
    351         if (kind == null) return null;
    352         // First try finding a valid primary
    353         EditType bestType = getBestValidType(state, kind, false, Integer.MIN_VALUE);
    354         if (bestType == null) {
    355             // No valid primary found, so expand search to secondary
    356             bestType = getBestValidType(state, kind, true, Integer.MIN_VALUE);
    357         }
    358         return insertChild(state, kind, bestType);
    359     }
    360 
    361     /**
    362      * Insert a new child of kind {@link DataKind} into the given
    363      * {@link RawContactDelta}, marked with the given {@link EditType}.
    364      */
    365     public static ValuesDelta insertChild(RawContactDelta state, DataKind kind, EditType type) {
    366         // Bail early if invalid kind
    367         if (kind == null) return null;
    368         final ContentValues after = new ContentValues();
    369 
    370         // Our parent CONTACT_ID is provided later
    371         after.put(Data.MIMETYPE, kind.mimeType);
    372 
    373         // Fill-in with any requested default values
    374         if (kind.defaultValues != null) {
    375             after.putAll(kind.defaultValues);
    376         }
    377 
    378         if (kind.typeColumn != null && type != null) {
    379             // Set type, if provided
    380             after.put(kind.typeColumn, type.rawValue);
    381         }
    382 
    383         final ValuesDelta child = ValuesDelta.fromAfter(after);
    384         state.addEntry(child);
    385         return child;
    386     }
    387 
    388     /**
    389      * Processing to trim any empty {@link ValuesDelta} and {@link RawContactDelta}
    390      * from the given {@link RawContactDeltaList}, assuming the given {@link AccountTypeManager}
    391      * dictates the structure for various fields. This method ignores rows not
    392      * described by the {@link AccountType}.
    393      */
    394     public static void trimEmpty(RawContactDeltaList set, AccountTypeManager accountTypes) {
    395         for (RawContactDelta state : set) {
    396             ValuesDelta values = state.getValues();
    397             final String accountType = values.getAsString(RawContacts.ACCOUNT_TYPE);
    398             final String dataSet = values.getAsString(RawContacts.DATA_SET);
    399             final AccountType type = accountTypes.getAccountType(accountType, dataSet);
    400             trimEmpty(state, type);
    401         }
    402     }
    403 
    404     public static boolean hasChanges(RawContactDeltaList set, AccountTypeManager accountTypes) {
    405         if (set.isMarkedForSplitting() || set.isMarkedForJoining()) {
    406             return true;
    407         }
    408 
    409         for (RawContactDelta state : set) {
    410             ValuesDelta values = state.getValues();
    411             final String accountType = values.getAsString(RawContacts.ACCOUNT_TYPE);
    412             final String dataSet = values.getAsString(RawContacts.DATA_SET);
    413             final AccountType type = accountTypes.getAccountType(accountType, dataSet);
    414             if (hasChanges(state, type)) {
    415                 return true;
    416             }
    417         }
    418         return false;
    419     }
    420 
    421     /**
    422      * Processing to trim any empty {@link ValuesDelta} rows from the given
    423      * {@link RawContactDelta}, assuming the given {@link AccountType} dictates
    424      * the structure for various fields. This method ignores rows not described
    425      * by the {@link AccountType}.
    426      */
    427     public static void trimEmpty(RawContactDelta state, AccountType accountType) {
    428         boolean hasValues = false;
    429 
    430         // Walk through entries for each well-known kind
    431         for (DataKind kind : accountType.getSortedDataKinds()) {
    432             final String mimeType = kind.mimeType;
    433             final ArrayList<ValuesDelta> entries = state.getMimeEntries(mimeType);
    434             if (entries == null) continue;
    435 
    436             for (ValuesDelta entry : entries) {
    437                 // Skip any values that haven't been touched
    438                 final boolean touched = entry.isInsert() || entry.isUpdate();
    439                 if (!touched) {
    440                     hasValues = true;
    441                     continue;
    442                 }
    443 
    444                 // Test and remove this row if empty and it isn't a photo from google
    445                 final boolean isGoogleAccount = TextUtils.equals(GoogleAccountType.ACCOUNT_TYPE,
    446                         state.getValues().getAsString(RawContacts.ACCOUNT_TYPE));
    447                 final boolean isPhoto = TextUtils.equals(Photo.CONTENT_ITEM_TYPE, kind.mimeType);
    448                 final boolean isGooglePhoto = isPhoto && isGoogleAccount;
    449 
    450                 if (RawContactModifier.isEmpty(entry, kind) && !isGooglePhoto) {
    451                     if (DEBUG) {
    452                         Log.v(TAG, "Trimming: " + entry.toString());
    453                     }
    454                     entry.markDeleted();
    455                 } else if (!entry.isFromTemplate()) {
    456                     hasValues = true;
    457                 }
    458             }
    459         }
    460         if (!hasValues) {
    461             // Trim overall entity if no children exist
    462             state.markDeleted();
    463         }
    464     }
    465 
    466     private static boolean hasChanges(RawContactDelta state, AccountType accountType) {
    467         for (DataKind kind : accountType.getSortedDataKinds()) {
    468             final String mimeType = kind.mimeType;
    469             final ArrayList<ValuesDelta> entries = state.getMimeEntries(mimeType);
    470             if (entries == null) continue;
    471 
    472             for (ValuesDelta entry : entries) {
    473                 // An empty Insert must be ignored, because it won't save anything (an example
    474                 // is an empty name that stays empty)
    475                 final boolean isRealInsert = entry.isInsert() && !isEmpty(entry, kind);
    476                 if (isRealInsert || entry.isUpdate() || entry.isDelete()) {
    477                     return true;
    478                 }
    479             }
    480         }
    481         return false;
    482     }
    483 
    484     /**
    485      * Test if the given {@link ValuesDelta} would be considered "empty" in
    486      * terms of {@link DataKind#fieldList}.
    487      */
    488     public static boolean isEmpty(ValuesDelta values, DataKind kind) {
    489         if (Photo.CONTENT_ITEM_TYPE.equals(kind.mimeType)) {
    490             return values.isInsert() && values.getAsByteArray(Photo.PHOTO) == null;
    491         }
    492 
    493         // No defined fields mean this row is always empty
    494         if (kind.fieldList == null) return true;
    495 
    496         for (EditField field : kind.fieldList) {
    497             // If any field has values, we're not empty
    498             final String value = values.getAsString(field.column);
    499             if (ContactsUtils.isGraphic(value)) {
    500                 return false;
    501             }
    502         }
    503 
    504         return true;
    505     }
    506 
    507     /**
    508      * Compares corresponding fields in values1 and values2. Only the fields
    509      * declared by the DataKind are taken into consideration.
    510      */
    511     protected static boolean areEqual(ValuesDelta values1, ContentValues values2, DataKind kind) {
    512         if (kind.fieldList == null) return false;
    513 
    514         for (EditField field : kind.fieldList) {
    515             final String value1 = values1.getAsString(field.column);
    516             final String value2 = values2.getAsString(field.column);
    517             if (!TextUtils.equals(value1, value2)) {
    518                 return false;
    519             }
    520         }
    521 
    522         return true;
    523     }
    524 
    525     /**
    526      * Parse the given {@link Bundle} into the given {@link RawContactDelta} state,
    527      * assuming the extras defined through {@link Intents}.
    528      */
    529     public static void parseExtras(Context context, AccountType accountType, RawContactDelta state,
    530             Bundle extras) {
    531         if (extras == null || extras.size() == 0) {
    532             // Bail early if no useful data
    533             return;
    534         }
    535 
    536         parseStructuredNameExtra(context, accountType, state, extras);
    537         parseStructuredPostalExtra(accountType, state, extras);
    538 
    539         {
    540             // Phone
    541             final DataKind kind = accountType.getKindForMimetype(Phone.CONTENT_ITEM_TYPE);
    542             parseExtras(state, kind, extras, Insert.PHONE_TYPE, Insert.PHONE, Phone.NUMBER);
    543             parseExtras(state, kind, extras, Insert.SECONDARY_PHONE_TYPE, Insert.SECONDARY_PHONE,
    544                     Phone.NUMBER);
    545             parseExtras(state, kind, extras, Insert.TERTIARY_PHONE_TYPE, Insert.TERTIARY_PHONE,
    546                     Phone.NUMBER);
    547         }
    548 
    549         {
    550             // Email
    551             final DataKind kind = accountType.getKindForMimetype(Email.CONTENT_ITEM_TYPE);
    552             parseExtras(state, kind, extras, Insert.EMAIL_TYPE, Insert.EMAIL, Email.DATA);
    553             parseExtras(state, kind, extras, Insert.SECONDARY_EMAIL_TYPE, Insert.SECONDARY_EMAIL,
    554                     Email.DATA);
    555             parseExtras(state, kind, extras, Insert.TERTIARY_EMAIL_TYPE, Insert.TERTIARY_EMAIL,
    556                     Email.DATA);
    557         }
    558 
    559         {
    560             // Im
    561             final DataKind kind = accountType.getKindForMimetype(Im.CONTENT_ITEM_TYPE);
    562             fixupLegacyImType(extras);
    563             parseExtras(state, kind, extras, Insert.IM_PROTOCOL, Insert.IM_HANDLE, Im.DATA);
    564         }
    565 
    566         // Organization
    567         final boolean hasOrg = extras.containsKey(Insert.COMPANY)
    568                 || extras.containsKey(Insert.JOB_TITLE);
    569         final DataKind kindOrg = accountType.getKindForMimetype(Organization.CONTENT_ITEM_TYPE);
    570         if (hasOrg && RawContactModifier.canInsert(state, kindOrg)) {
    571             final ValuesDelta child = RawContactModifier.insertChild(state, kindOrg);
    572 
    573             final String company = extras.getString(Insert.COMPANY);
    574             if (ContactsUtils.isGraphic(company)) {
    575                 child.put(Organization.COMPANY, company);
    576             }
    577 
    578             final String title = extras.getString(Insert.JOB_TITLE);
    579             if (ContactsUtils.isGraphic(title)) {
    580                 child.put(Organization.TITLE, title);
    581             }
    582         }
    583 
    584         // Notes
    585         final boolean hasNotes = extras.containsKey(Insert.NOTES);
    586         final DataKind kindNotes = accountType.getKindForMimetype(Note.CONTENT_ITEM_TYPE);
    587         if (hasNotes && RawContactModifier.canInsert(state, kindNotes)) {
    588             final ValuesDelta child = RawContactModifier.insertChild(state, kindNotes);
    589 
    590             final String notes = extras.getString(Insert.NOTES);
    591             if (ContactsUtils.isGraphic(notes)) {
    592                 child.put(Note.NOTE, notes);
    593             }
    594         }
    595 
    596         // Arbitrary additional data
    597         ArrayList<ContentValues> values = extras.getParcelableArrayList(Insert.DATA);
    598         if (values != null) {
    599             parseValues(state, accountType, values);
    600         }
    601     }
    602 
    603     private static void parseStructuredNameExtra(
    604             Context context, AccountType accountType, RawContactDelta state, Bundle extras) {
    605         // StructuredName
    606         RawContactModifier.ensureKindExists(state, accountType, StructuredName.CONTENT_ITEM_TYPE);
    607         final ValuesDelta child = state.getPrimaryEntry(StructuredName.CONTENT_ITEM_TYPE);
    608 
    609         final String name = extras.getString(Insert.NAME);
    610         if (ContactsUtils.isGraphic(name)) {
    611             final DataKind kind = accountType.getKindForMimetype(StructuredName.CONTENT_ITEM_TYPE);
    612             boolean supportsDisplayName = false;
    613             if (kind.fieldList != null) {
    614                 for (EditField field : kind.fieldList) {
    615                     if (StructuredName.DISPLAY_NAME.equals(field.column)) {
    616                         supportsDisplayName = true;
    617                         break;
    618                     }
    619                 }
    620             }
    621 
    622             if (supportsDisplayName) {
    623                 child.put(StructuredName.DISPLAY_NAME, name);
    624             } else {
    625                 Uri uri = ContactsContract.AUTHORITY_URI.buildUpon()
    626                         .appendPath("complete_name")
    627                         .appendQueryParameter(StructuredName.DISPLAY_NAME, name)
    628                         .build();
    629                 Cursor cursor = context.getContentResolver().query(uri,
    630                         new String[]{
    631                                 StructuredName.PREFIX,
    632                                 StructuredName.GIVEN_NAME,
    633                                 StructuredName.MIDDLE_NAME,
    634                                 StructuredName.FAMILY_NAME,
    635                                 StructuredName.SUFFIX,
    636                         }, null, null, null);
    637 
    638                 if (cursor != null) {
    639                     try {
    640                         if (cursor.moveToFirst()) {
    641                             child.put(StructuredName.PREFIX, cursor.getString(0));
    642                             child.put(StructuredName.GIVEN_NAME, cursor.getString(1));
    643                             child.put(StructuredName.MIDDLE_NAME, cursor.getString(2));
    644                             child.put(StructuredName.FAMILY_NAME, cursor.getString(3));
    645                             child.put(StructuredName.SUFFIX, cursor.getString(4));
    646                         }
    647                     } finally {
    648                         cursor.close();
    649                     }
    650                 }
    651             }
    652         }
    653 
    654         final String phoneticName = extras.getString(Insert.PHONETIC_NAME);
    655         if (ContactsUtils.isGraphic(phoneticName)) {
    656             StructuredNameDataItem dataItem = NameConverter.parsePhoneticName(phoneticName, null);
    657             child.put(StructuredName.PHONETIC_FAMILY_NAME, dataItem.getPhoneticFamilyName());
    658             child.put(StructuredName.PHONETIC_MIDDLE_NAME, dataItem.getPhoneticMiddleName());
    659             child.put(StructuredName.PHONETIC_GIVEN_NAME, dataItem.getPhoneticGivenName());
    660         }
    661     }
    662 
    663     private static void parseStructuredPostalExtra(
    664             AccountType accountType, RawContactDelta state, Bundle extras) {
    665         // StructuredPostal
    666         final DataKind kind = accountType.getKindForMimetype(StructuredPostal.CONTENT_ITEM_TYPE);
    667         final ValuesDelta child = parseExtras(state, kind, extras, Insert.POSTAL_TYPE,
    668                 Insert.POSTAL, StructuredPostal.FORMATTED_ADDRESS);
    669         String address = child == null ? null
    670                 : child.getAsString(StructuredPostal.FORMATTED_ADDRESS);
    671         if (!TextUtils.isEmpty(address)) {
    672             boolean supportsFormatted = false;
    673             if (kind.fieldList != null) {
    674                 for (EditField field : kind.fieldList) {
    675                     if (StructuredPostal.FORMATTED_ADDRESS.equals(field.column)) {
    676                         supportsFormatted = true;
    677                         break;
    678                     }
    679                 }
    680             }
    681 
    682             if (!supportsFormatted) {
    683                 child.put(StructuredPostal.STREET, address);
    684                 child.putNull(StructuredPostal.FORMATTED_ADDRESS);
    685             }
    686         }
    687     }
    688 
    689     private static void parseValues(
    690             RawContactDelta state, AccountType accountType,
    691             ArrayList<ContentValues> dataValueList) {
    692         for (ContentValues values : dataValueList) {
    693             String mimeType = values.getAsString(Data.MIMETYPE);
    694             if (TextUtils.isEmpty(mimeType)) {
    695                 Log.e(TAG, "Mimetype is required. Ignoring: " + values);
    696                 continue;
    697             }
    698 
    699             // Won't override the contact name
    700             if (StructuredName.CONTENT_ITEM_TYPE.equals(mimeType)) {
    701                 continue;
    702             } else if (Phone.CONTENT_ITEM_TYPE.equals(mimeType)) {
    703                 values.remove(PhoneDataItem.KEY_FORMATTED_PHONE_NUMBER);
    704                 final Integer type = values.getAsInteger(Phone.TYPE);
    705                 // If the provided phone number provides a custom phone type but not a label,
    706                 // replace it with mobile (by default) to avoid the "Enter custom label" from
    707                 // popping up immediately upon entering the ContactEditorFragment
    708                 if (type != null && type == Phone.TYPE_CUSTOM &&
    709                         TextUtils.isEmpty(values.getAsString(Phone.LABEL))) {
    710                     values.put(Phone.TYPE, Phone.TYPE_MOBILE);
    711                 }
    712             }
    713 
    714             DataKind kind = accountType.getKindForMimetype(mimeType);
    715             if (kind == null) {
    716                 Log.e(TAG, "Mimetype not supported for account type "
    717                         + accountType.getAccountTypeAndDataSet() + ". Ignoring: " + values);
    718                 continue;
    719             }
    720 
    721             ValuesDelta entry = ValuesDelta.fromAfter(values);
    722             if (isEmpty(entry, kind)) {
    723                 continue;
    724             }
    725 
    726             ArrayList<ValuesDelta> entries = state.getMimeEntries(mimeType);
    727 
    728             if ((kind.typeOverallMax != 1) || GroupMembership.CONTENT_ITEM_TYPE.equals(mimeType)) {
    729                 // Check for duplicates
    730                 boolean addEntry = true;
    731                 int count = 0;
    732                 if (entries != null && entries.size() > 0) {
    733                     for (ValuesDelta delta : entries) {
    734                         if (!delta.isDelete()) {
    735                             if (areEqual(delta, values, kind)) {
    736                                 addEntry = false;
    737                                 break;
    738                             }
    739                             count++;
    740                         }
    741                     }
    742                 }
    743 
    744                 if (kind.typeOverallMax != -1 && count >= kind.typeOverallMax) {
    745                     Log.e(TAG, "Mimetype allows at most " + kind.typeOverallMax
    746                             + " entries. Ignoring: " + values);
    747                     addEntry = false;
    748                 }
    749 
    750                 if (addEntry) {
    751                     addEntry = adjustType(entry, entries, kind);
    752                 }
    753 
    754                 if (addEntry) {
    755                     state.addEntry(entry);
    756                 }
    757             } else {
    758                 // Non-list entries should not be overridden
    759                 boolean addEntry = true;
    760                 if (entries != null && entries.size() > 0) {
    761                     for (ValuesDelta delta : entries) {
    762                         if (!delta.isDelete() && !isEmpty(delta, kind)) {
    763                             addEntry = false;
    764                             break;
    765                         }
    766                     }
    767                     if (addEntry) {
    768                         for (ValuesDelta delta : entries) {
    769                             delta.markDeleted();
    770                         }
    771                     }
    772                 }
    773 
    774                 if (addEntry) {
    775                     addEntry = adjustType(entry, entries, kind);
    776                 }
    777 
    778                 if (addEntry) {
    779                     state.addEntry(entry);
    780                 } else if (Note.CONTENT_ITEM_TYPE.equals(mimeType)){
    781                     // Note is most likely to contain large amounts of text
    782                     // that we don't want to drop on the ground.
    783                     for (ValuesDelta delta : entries) {
    784                         if (!isEmpty(delta, kind)) {
    785                             delta.put(Note.NOTE, delta.getAsString(Note.NOTE) + "\n"
    786                                     + values.getAsString(Note.NOTE));
    787                             break;
    788                         }
    789                     }
    790                 } else {
    791                     Log.e(TAG, "Will not override mimetype " + mimeType + ". Ignoring: "
    792                             + values);
    793                 }
    794             }
    795         }
    796     }
    797 
    798     /**
    799      * Checks if the data kind allows addition of another entry (e.g. Exchange only
    800      * supports two "work" phone numbers).  If not, tries to switch to one of the
    801      * unused types.  If successful, returns true.
    802      */
    803     private static boolean adjustType(
    804             ValuesDelta entry, ArrayList<ValuesDelta> entries, DataKind kind) {
    805         if (kind.typeColumn == null || kind.typeList == null || kind.typeList.size() == 0) {
    806             return true;
    807         }
    808 
    809         Integer typeInteger = entry.getAsInteger(kind.typeColumn);
    810         int type = typeInteger != null ? typeInteger : kind.typeList.get(0).rawValue;
    811 
    812         if (isTypeAllowed(type, entries, kind)) {
    813             entry.put(kind.typeColumn, type);
    814             return true;
    815         }
    816 
    817         // Specified type is not allowed - choose the first available type that is allowed
    818         int size = kind.typeList.size();
    819         for (int i = 0; i < size; i++) {
    820             EditType editType = kind.typeList.get(i);
    821             if (isTypeAllowed(editType.rawValue, entries, kind)) {
    822                 entry.put(kind.typeColumn, editType.rawValue);
    823                 return true;
    824             }
    825         }
    826 
    827         return false;
    828     }
    829 
    830     /**
    831      * Checks if a new entry of the specified type can be added to the raw
    832      * contact. For example, Exchange only supports two "work" phone numbers, so
    833      * addition of a third would not be allowed.
    834      */
    835     private static boolean isTypeAllowed(int type, ArrayList<ValuesDelta> entries, DataKind kind) {
    836         int max = 0;
    837         int size = kind.typeList.size();
    838         for (int i = 0; i < size; i++) {
    839             EditType editType = kind.typeList.get(i);
    840             if (editType.rawValue == type) {
    841                 max = editType.specificMax;
    842                 break;
    843             }
    844         }
    845 
    846         if (max == 0) {
    847             // This type is not allowed at all
    848             return false;
    849         }
    850 
    851         if (max == -1) {
    852             // Unlimited instances of this type are allowed
    853             return true;
    854         }
    855 
    856         return getEntryCountByType(entries, kind.typeColumn, type) < max;
    857     }
    858 
    859     /**
    860      * Counts occurrences of the specified type in the supplied entry list.
    861      *
    862      * @return The count of occurrences of the type in the entry list. 0 if entries is
    863      * {@literal null}
    864      */
    865     private static int getEntryCountByType(ArrayList<ValuesDelta> entries, String typeColumn,
    866             int type) {
    867         int count = 0;
    868         if (entries != null) {
    869             for (ValuesDelta entry : entries) {
    870                 Integer typeInteger = entry.getAsInteger(typeColumn);
    871                 if (typeInteger != null && typeInteger == type) {
    872                     count++;
    873                 }
    874             }
    875         }
    876         return count;
    877     }
    878 
    879     /**
    880      * Attempt to parse legacy {@link Insert#IM_PROTOCOL} values, replacing them
    881      * with updated values.
    882      */
    883     @SuppressWarnings("deprecation")
    884     private static void fixupLegacyImType(Bundle bundle) {
    885         final String encodedString = bundle.getString(Insert.IM_PROTOCOL);
    886         if (encodedString == null) return;
    887 
    888         try {
    889             final Object protocol = android.provider.Contacts.ContactMethods
    890                     .decodeImProtocol(encodedString);
    891             if (protocol instanceof Integer) {
    892                 bundle.putInt(Insert.IM_PROTOCOL, (Integer)protocol);
    893             } else {
    894                 bundle.putString(Insert.IM_PROTOCOL, (String)protocol);
    895             }
    896         } catch (IllegalArgumentException e) {
    897             // Ignore exception when legacy parser fails
    898         }
    899     }
    900 
    901     /**
    902      * Parse a specific entry from the given {@link Bundle} and insert into the
    903      * given {@link RawContactDelta}. Silently skips the insert when missing value
    904      * or no valid {@link EditType} found.
    905      *
    906      * @param typeExtra {@link Bundle} key that holds the incoming
    907      *            {@link EditType#rawValue} value.
    908      * @param valueExtra {@link Bundle} key that holds the incoming value.
    909      * @param valueColumn Column to write value into {@link ValuesDelta}.
    910      */
    911     public static ValuesDelta parseExtras(RawContactDelta state, DataKind kind, Bundle extras,
    912             String typeExtra, String valueExtra, String valueColumn) {
    913         final CharSequence value = extras.getCharSequence(valueExtra);
    914 
    915         // Bail early if account type doesn't handle this MIME type
    916         if (kind == null) return null;
    917 
    918         // Bail when can't insert type, or value missing
    919         final boolean canInsert = RawContactModifier.canInsert(state, kind);
    920         final boolean validValue = (value != null && TextUtils.isGraphic(value));
    921         if (!validValue || !canInsert) return null;
    922 
    923         // Find exact type when requested, otherwise best available type
    924         final boolean hasType = extras.containsKey(typeExtra);
    925         final int typeValue = extras.getInt(typeExtra, hasType ? BaseTypes.TYPE_CUSTOM
    926                 : Integer.MIN_VALUE);
    927         final EditType editType = RawContactModifier.getBestValidType(state, kind, true, typeValue);
    928 
    929         // Create data row and fill with value
    930         final ValuesDelta child = RawContactModifier.insertChild(state, kind, editType);
    931         child.put(valueColumn, value.toString());
    932 
    933         if (editType != null && editType.customColumn != null) {
    934             // Write down label when custom type picked
    935             final String customType = extras.getString(typeExtra);
    936             child.put(editType.customColumn, customType);
    937         }
    938 
    939         return child;
    940     }
    941 
    942     /**
    943      * Generic mime types with type support (e.g. TYPE_HOME).
    944      * Here, "type support" means if the data kind has CommonColumns#TYPE or not. Data kinds which
    945      * have their own migrate methods aren't listed here.
    946      */
    947     private static final Set<String> sGenericMimeTypesWithTypeSupport = new HashSet<String>(
    948             Arrays.asList(Phone.CONTENT_ITEM_TYPE,
    949                     Email.CONTENT_ITEM_TYPE,
    950                     Im.CONTENT_ITEM_TYPE,
    951                     Nickname.CONTENT_ITEM_TYPE,
    952                     Website.CONTENT_ITEM_TYPE,
    953                     Relation.CONTENT_ITEM_TYPE,
    954                     SipAddress.CONTENT_ITEM_TYPE));
    955     private static final Set<String> sGenericMimeTypesWithoutTypeSupport = new HashSet<String>(
    956             Arrays.asList(Organization.CONTENT_ITEM_TYPE,
    957                     Note.CONTENT_ITEM_TYPE,
    958                     Photo.CONTENT_ITEM_TYPE,
    959                     GroupMembership.CONTENT_ITEM_TYPE));
    960     // CommonColumns.TYPE cannot be accessed as it is protected interface, so use
    961     // Phone.TYPE instead.
    962     private static final String COLUMN_FOR_TYPE  = Phone.TYPE;
    963     private static final String COLUMN_FOR_LABEL  = Phone.LABEL;
    964     private static final int TYPE_CUSTOM = Phone.TYPE_CUSTOM;
    965 
    966     /**
    967      * Migrates old RawContactDelta to newly created one with a new restriction supplied from
    968      * newAccountType.
    969      *
    970      * This is only for account switch during account creation (which must be insert operation).
    971      */
    972     public static void migrateStateForNewContact(Context context,
    973             RawContactDelta oldState, RawContactDelta newState,
    974             AccountType oldAccountType, AccountType newAccountType) {
    975         if (newAccountType == oldAccountType) {
    976             // Just copying all data in oldState isn't enough, but we can still rely on a lot of
    977             // shortcuts.
    978             for (DataKind kind : newAccountType.getSortedDataKinds()) {
    979                 final String mimeType = kind.mimeType;
    980                 // The fields with short/long form capability must be treated properly.
    981                 if (StructuredName.CONTENT_ITEM_TYPE.equals(mimeType)) {
    982                     migrateStructuredName(context, oldState, newState, kind);
    983                 } else {
    984                     List<ValuesDelta> entryList = oldState.getMimeEntries(mimeType);
    985                     if (entryList != null && !entryList.isEmpty()) {
    986                         for (ValuesDelta entry : entryList) {
    987                             ContentValues values = entry.getAfter();
    988                             if (values != null) {
    989                                 newState.addEntry(ValuesDelta.fromAfter(values));
    990                             }
    991                         }
    992                     }
    993                 }
    994             }
    995         } else {
    996             // Migrate data supported by the new account type.
    997             // All the other data inside oldState are silently dropped.
    998             for (DataKind kind : newAccountType.getSortedDataKinds()) {
    999                 if (!kind.editable) continue;
   1000                 final String mimeType = kind.mimeType;
   1001                 if (DataKind.PSEUDO_MIME_TYPE_DISPLAY_NAME.equals(mimeType)
   1002                         || DataKind.PSEUDO_MIME_TYPE_PHONETIC_NAME.equals(mimeType)) {
   1003                     // Ignore pseudo data.
   1004                     continue;
   1005                 } else if (StructuredName.CONTENT_ITEM_TYPE.equals(mimeType)) {
   1006                     migrateStructuredName(context, oldState, newState, kind);
   1007                 } else if (StructuredPostal.CONTENT_ITEM_TYPE.equals(mimeType)) {
   1008                     migratePostal(oldState, newState, kind);
   1009                 } else if (Event.CONTENT_ITEM_TYPE.equals(mimeType)) {
   1010                     migrateEvent(oldState, newState, kind, null /* default Year */);
   1011                 } else if (sGenericMimeTypesWithoutTypeSupport.contains(mimeType)) {
   1012                     migrateGenericWithoutTypeColumn(oldState, newState, kind);
   1013                 } else if (sGenericMimeTypesWithTypeSupport.contains(mimeType)) {
   1014                     migrateGenericWithTypeColumn(oldState, newState, kind);
   1015                 } else {
   1016                     throw new IllegalStateException("Unexpected editable mime-type: " + mimeType);
   1017                 }
   1018             }
   1019         }
   1020     }
   1021 
   1022     /**
   1023      * Checks {@link DataKind#isList} and {@link DataKind#typeOverallMax}, and restricts
   1024      * the number of entries (ValuesDelta) inside newState.
   1025      */
   1026     private static ArrayList<ValuesDelta> ensureEntryMaxSize(RawContactDelta newState,
   1027             DataKind kind, ArrayList<ValuesDelta> mimeEntries) {
   1028         if (mimeEntries == null) {
   1029             return null;
   1030         }
   1031 
   1032         final int typeOverallMax = kind.typeOverallMax;
   1033         if (typeOverallMax >= 0 && (mimeEntries.size() > typeOverallMax)) {
   1034             ArrayList<ValuesDelta> newMimeEntries = new ArrayList<ValuesDelta>(typeOverallMax);
   1035             for (int i = 0; i < typeOverallMax; i++) {
   1036                 newMimeEntries.add(mimeEntries.get(i));
   1037             }
   1038             mimeEntries = newMimeEntries;
   1039         }
   1040         return mimeEntries;
   1041     }
   1042 
   1043     /** @hide Public only for testing. */
   1044     public static void migrateStructuredName(
   1045             Context context, RawContactDelta oldState, RawContactDelta newState,
   1046             DataKind newDataKind) {
   1047         final ContentValues values =
   1048                 oldState.getPrimaryEntry(StructuredName.CONTENT_ITEM_TYPE).getAfter();
   1049         if (values == null) {
   1050             return;
   1051         }
   1052 
   1053         boolean supportDisplayName = false;
   1054         boolean supportPhoneticFullName = false;
   1055         boolean supportPhoneticFamilyName = false;
   1056         boolean supportPhoneticMiddleName = false;
   1057         boolean supportPhoneticGivenName = false;
   1058         for (EditField editField : newDataKind.fieldList) {
   1059             if (StructuredName.DISPLAY_NAME.equals(editField.column)) {
   1060                 supportDisplayName = true;
   1061             }
   1062             if (DataKind.PSEUDO_COLUMN_PHONETIC_NAME.equals(editField.column)) {
   1063                 supportPhoneticFullName = true;
   1064             }
   1065             if (StructuredName.PHONETIC_FAMILY_NAME.equals(editField.column)) {
   1066                 supportPhoneticFamilyName = true;
   1067             }
   1068             if (StructuredName.PHONETIC_MIDDLE_NAME.equals(editField.column)) {
   1069                 supportPhoneticMiddleName = true;
   1070             }
   1071             if (StructuredName.PHONETIC_GIVEN_NAME.equals(editField.column)) {
   1072                 supportPhoneticGivenName = true;
   1073             }
   1074         }
   1075 
   1076         // DISPLAY_NAME <-> PREFIX, GIVEN_NAME, MIDDLE_NAME, FAMILY_NAME, SUFFIX
   1077         final String displayName = values.getAsString(StructuredName.DISPLAY_NAME);
   1078         if (!TextUtils.isEmpty(displayName)) {
   1079             if (!supportDisplayName) {
   1080                 // Old data has a display name, while the new account doesn't allow it.
   1081                 NameConverter.displayNameToStructuredName(context, displayName, values);
   1082 
   1083                 // We don't want to migrate unseen data which may confuse users after the creation.
   1084                 values.remove(StructuredName.DISPLAY_NAME);
   1085             }
   1086         } else {
   1087             if (supportDisplayName) {
   1088                 // Old data does not have display name, while the new account requires it.
   1089                 values.put(StructuredName.DISPLAY_NAME,
   1090                         NameConverter.structuredNameToDisplayName(context, values));
   1091                 for (String field : NameConverter.STRUCTURED_NAME_FIELDS) {
   1092                     values.remove(field);
   1093                 }
   1094             }
   1095         }
   1096 
   1097         // Phonetic (full) name <-> PHONETIC_FAMILY_NAME, PHONETIC_MIDDLE_NAME, PHONETIC_GIVEN_NAME
   1098         final String phoneticFullName = values.getAsString(DataKind.PSEUDO_COLUMN_PHONETIC_NAME);
   1099         if (!TextUtils.isEmpty(phoneticFullName)) {
   1100             if (!supportPhoneticFullName) {
   1101                 // Old data has a phonetic (full) name, while the new account doesn't allow it.
   1102                 final StructuredNameDataItem tmpItem =
   1103                         NameConverter.parsePhoneticName(phoneticFullName, null);
   1104                 values.remove(DataKind.PSEUDO_COLUMN_PHONETIC_NAME);
   1105                 if (supportPhoneticFamilyName) {
   1106                     values.put(StructuredName.PHONETIC_FAMILY_NAME,
   1107                             tmpItem.getPhoneticFamilyName());
   1108                 } else {
   1109                     values.remove(StructuredName.PHONETIC_FAMILY_NAME);
   1110                 }
   1111                 if (supportPhoneticMiddleName) {
   1112                     values.put(StructuredName.PHONETIC_MIDDLE_NAME,
   1113                             tmpItem.getPhoneticMiddleName());
   1114                 } else {
   1115                     values.remove(StructuredName.PHONETIC_MIDDLE_NAME);
   1116                 }
   1117                 if (supportPhoneticGivenName) {
   1118                     values.put(StructuredName.PHONETIC_GIVEN_NAME,
   1119                             tmpItem.getPhoneticGivenName());
   1120                 } else {
   1121                     values.remove(StructuredName.PHONETIC_GIVEN_NAME);
   1122                 }
   1123             }
   1124         } else {
   1125             if (supportPhoneticFullName) {
   1126                 // Old data does not have a phonetic (full) name, while the new account requires it.
   1127                 values.put(DataKind.PSEUDO_COLUMN_PHONETIC_NAME,
   1128                         NameConverter.buildPhoneticName(
   1129                                 values.getAsString(StructuredName.PHONETIC_FAMILY_NAME),
   1130                                 values.getAsString(StructuredName.PHONETIC_MIDDLE_NAME),
   1131                                 values.getAsString(StructuredName.PHONETIC_GIVEN_NAME)));
   1132             }
   1133             if (!supportPhoneticFamilyName) {
   1134                 values.remove(StructuredName.PHONETIC_FAMILY_NAME);
   1135             }
   1136             if (!supportPhoneticMiddleName) {
   1137                 values.remove(StructuredName.PHONETIC_MIDDLE_NAME);
   1138             }
   1139             if (!supportPhoneticGivenName) {
   1140                 values.remove(StructuredName.PHONETIC_GIVEN_NAME);
   1141             }
   1142         }
   1143 
   1144         newState.addEntry(ValuesDelta.fromAfter(values));
   1145     }
   1146 
   1147     /** @hide Public only for testing. */
   1148     public static void migratePostal(RawContactDelta oldState, RawContactDelta newState,
   1149             DataKind newDataKind) {
   1150         final ArrayList<ValuesDelta> mimeEntries = ensureEntryMaxSize(newState, newDataKind,
   1151                 oldState.getMimeEntries(StructuredPostal.CONTENT_ITEM_TYPE));
   1152         if (mimeEntries == null || mimeEntries.isEmpty()) {
   1153             return;
   1154         }
   1155 
   1156         boolean supportFormattedAddress = false;
   1157         boolean supportStreet = false;
   1158         final String firstColumn = newDataKind.fieldList.get(0).column;
   1159         for (EditField editField : newDataKind.fieldList) {
   1160             if (StructuredPostal.FORMATTED_ADDRESS.equals(editField.column)) {
   1161                 supportFormattedAddress = true;
   1162             }
   1163             if (StructuredPostal.STREET.equals(editField.column)) {
   1164                 supportStreet = true;
   1165             }
   1166         }
   1167 
   1168         final Set<Integer> supportedTypes = new HashSet<Integer>();
   1169         if (newDataKind.typeList != null && !newDataKind.typeList.isEmpty()) {
   1170             for (EditType editType : newDataKind.typeList) {
   1171                 supportedTypes.add(editType.rawValue);
   1172             }
   1173         }
   1174 
   1175         for (ValuesDelta entry : mimeEntries) {
   1176             final ContentValues values = entry.getAfter();
   1177             if (values == null) {
   1178                 continue;
   1179             }
   1180             final Integer oldType = values.getAsInteger(StructuredPostal.TYPE);
   1181             if (!supportedTypes.contains(oldType)) {
   1182                 int defaultType;
   1183                 if (newDataKind.defaultValues != null) {
   1184                     defaultType = newDataKind.defaultValues.getAsInteger(StructuredPostal.TYPE);
   1185                 } else {
   1186                     defaultType = newDataKind.typeList.get(0).rawValue;
   1187                 }
   1188                 values.put(StructuredPostal.TYPE, defaultType);
   1189                 if (oldType != null && oldType == StructuredPostal.TYPE_CUSTOM) {
   1190                     values.remove(StructuredPostal.LABEL);
   1191                 }
   1192             }
   1193 
   1194             final String formattedAddress = values.getAsString(StructuredPostal.FORMATTED_ADDRESS);
   1195             if (!TextUtils.isEmpty(formattedAddress)) {
   1196                 if (!supportFormattedAddress) {
   1197                     // Old data has a formatted address, while the new account doesn't allow it.
   1198                     values.remove(StructuredPostal.FORMATTED_ADDRESS);
   1199 
   1200                     // Unlike StructuredName we don't have logic to split it, so first
   1201                     // try to use street field and. If the new account doesn't have one,
   1202                     // then select first one anyway.
   1203                     if (supportStreet) {
   1204                         values.put(StructuredPostal.STREET, formattedAddress);
   1205                     } else {
   1206                         values.put(firstColumn, formattedAddress);
   1207                     }
   1208                 }
   1209             } else {
   1210                 if (supportFormattedAddress) {
   1211                     // Old data does not have formatted address, while the new account requires it.
   1212                     // Unlike StructuredName we don't have logic to join multiple address values.
   1213                     // Use poor join heuristics for now.
   1214                     String[] structuredData;
   1215                     final boolean useJapaneseOrder =
   1216                             Locale.JAPANESE.getLanguage().equals(Locale.getDefault().getLanguage());
   1217                     if (useJapaneseOrder) {
   1218                         structuredData = new String[] {
   1219                                 values.getAsString(StructuredPostal.COUNTRY),
   1220                                 values.getAsString(StructuredPostal.POSTCODE),
   1221                                 values.getAsString(StructuredPostal.REGION),
   1222                                 values.getAsString(StructuredPostal.CITY),
   1223                                 values.getAsString(StructuredPostal.NEIGHBORHOOD),
   1224                                 values.getAsString(StructuredPostal.STREET),
   1225                                 values.getAsString(StructuredPostal.POBOX) };
   1226                     } else {
   1227                         structuredData = new String[] {
   1228                                 values.getAsString(StructuredPostal.POBOX),
   1229                                 values.getAsString(StructuredPostal.STREET),
   1230                                 values.getAsString(StructuredPostal.NEIGHBORHOOD),
   1231                                 values.getAsString(StructuredPostal.CITY),
   1232                                 values.getAsString(StructuredPostal.REGION),
   1233                                 values.getAsString(StructuredPostal.POSTCODE),
   1234                                 values.getAsString(StructuredPostal.COUNTRY) };
   1235                     }
   1236                     final StringBuilder builder = new StringBuilder();
   1237                     for (String elem : structuredData) {
   1238                         if (!TextUtils.isEmpty(elem)) {
   1239                             builder.append(elem + "\n");
   1240                         }
   1241                     }
   1242                     values.put(StructuredPostal.FORMATTED_ADDRESS, builder.toString());
   1243 
   1244                     values.remove(StructuredPostal.POBOX);
   1245                     values.remove(StructuredPostal.STREET);
   1246                     values.remove(StructuredPostal.NEIGHBORHOOD);
   1247                     values.remove(StructuredPostal.CITY);
   1248                     values.remove(StructuredPostal.REGION);
   1249                     values.remove(StructuredPostal.POSTCODE);
   1250                     values.remove(StructuredPostal.COUNTRY);
   1251                 }
   1252             }
   1253 
   1254             newState.addEntry(ValuesDelta.fromAfter(values));
   1255         }
   1256     }
   1257 
   1258     /** @hide Public only for testing. */
   1259     public static void migrateEvent(RawContactDelta oldState, RawContactDelta newState,
   1260             DataKind newDataKind, Integer defaultYear) {
   1261         final ArrayList<ValuesDelta> mimeEntries = ensureEntryMaxSize(newState, newDataKind,
   1262                 oldState.getMimeEntries(Event.CONTENT_ITEM_TYPE));
   1263         if (mimeEntries == null || mimeEntries.isEmpty()) {
   1264             return;
   1265         }
   1266 
   1267         final SparseArray<EventEditType> allowedTypes = new SparseArray<EventEditType>();
   1268         for (EditType editType : newDataKind.typeList) {
   1269             allowedTypes.put(editType.rawValue, (EventEditType) editType);
   1270         }
   1271         for (ValuesDelta entry : mimeEntries) {
   1272             final ContentValues values = entry.getAfter();
   1273             if (values == null) {
   1274                 continue;
   1275             }
   1276             final String dateString = values.getAsString(Event.START_DATE);
   1277             final Integer type = values.getAsInteger(Event.TYPE);
   1278             if (type != null && (allowedTypes.indexOfKey(type) >= 0)
   1279                     && !TextUtils.isEmpty(dateString)) {
   1280                 EventEditType suitableType = allowedTypes.get(type);
   1281 
   1282                 final ParsePosition position = new ParsePosition(0);
   1283                 boolean yearOptional = false;
   1284                 Date date = CommonDateUtils.DATE_AND_TIME_FORMAT.parse(dateString, position);
   1285                 if (date == null) {
   1286                     yearOptional = true;
   1287                     date = CommonDateUtils.NO_YEAR_DATE_FORMAT.parse(dateString, position);
   1288                 }
   1289                 if (date != null) {
   1290                     if (yearOptional && !suitableType.isYearOptional()) {
   1291                         // The new EditType doesn't allow optional year. Supply default.
   1292                         final Calendar calendar = Calendar.getInstance(DateUtils.UTC_TIMEZONE,
   1293                                 Locale.US);
   1294                         if (defaultYear == null) {
   1295                             defaultYear = calendar.get(Calendar.YEAR);
   1296                         }
   1297                         calendar.setTime(date);
   1298                         final int month = calendar.get(Calendar.MONTH);
   1299                         final int day = calendar.get(Calendar.DAY_OF_MONTH);
   1300                         // Exchange requires 8:00 for birthdays
   1301                         calendar.set(defaultYear, month, day,
   1302                                 CommonDateUtils.DEFAULT_HOUR, 0, 0);
   1303                         values.put(Event.START_DATE,
   1304                                 CommonDateUtils.FULL_DATE_FORMAT.format(calendar.getTime()));
   1305                     }
   1306                 }
   1307                 newState.addEntry(ValuesDelta.fromAfter(values));
   1308             } else {
   1309                 // Just drop it.
   1310             }
   1311         }
   1312     }
   1313 
   1314     /** @hide Public only for testing. */
   1315     public static void migrateGenericWithoutTypeColumn(
   1316             RawContactDelta oldState, RawContactDelta newState, DataKind newDataKind) {
   1317         final ArrayList<ValuesDelta> mimeEntries = ensureEntryMaxSize(newState, newDataKind,
   1318                 oldState.getMimeEntries(newDataKind.mimeType));
   1319         if (mimeEntries == null || mimeEntries.isEmpty()) {
   1320             return;
   1321         }
   1322 
   1323         for (ValuesDelta entry : mimeEntries) {
   1324             ContentValues values = entry.getAfter();
   1325             if (values != null) {
   1326                 newState.addEntry(ValuesDelta.fromAfter(values));
   1327             }
   1328         }
   1329     }
   1330 
   1331     /** @hide Public only for testing. */
   1332     public static void migrateGenericWithTypeColumn(
   1333             RawContactDelta oldState, RawContactDelta newState, DataKind newDataKind) {
   1334         final ArrayList<ValuesDelta> mimeEntries = oldState.getMimeEntries(newDataKind.mimeType);
   1335         if (mimeEntries == null || mimeEntries.isEmpty()) {
   1336             return;
   1337         }
   1338 
   1339         // Note that type specified with the old account may be invalid with the new account, while
   1340         // we want to preserve its data as much as possible. e.g. if a user typed a phone number
   1341         // with a type which is valid with an old account but not with a new account, the user
   1342         // probably wants to have the number with default type, rather than seeing complete data
   1343         // loss.
   1344         //
   1345         // Specifically, this method works as follows:
   1346         // 1. detect defaultType
   1347         // 2. prepare constants & variables for iteration
   1348         // 3. iterate over mimeEntries:
   1349         // 3.1 stop iteration if total number of mimeEntries reached typeOverallMax specified in
   1350         //     DataKind
   1351         // 3.2 replace unallowed types with defaultType
   1352         // 3.3 check if the number of entries is below specificMax specified in AccountType
   1353 
   1354         // Here, defaultType can be supplied in two ways
   1355         // - via kind.defaultValues
   1356         // - via kind.typeList.get(0).rawValue
   1357         Integer defaultType = null;
   1358         if (newDataKind.defaultValues != null) {
   1359             defaultType = newDataKind.defaultValues.getAsInteger(COLUMN_FOR_TYPE);
   1360         }
   1361         final Set<Integer> allowedTypes = new HashSet<Integer>();
   1362         // key: type, value: the number of entries allowed for the type (specificMax)
   1363         final SparseIntArray typeSpecificMaxMap = new SparseIntArray();
   1364         if (defaultType != null) {
   1365             allowedTypes.add(defaultType);
   1366             typeSpecificMaxMap.put(defaultType, -1);
   1367         }
   1368         // Note: typeList may be used in different purposes when defaultValues are specified.
   1369         // Especially in IM, typeList contains available protocols (e.g. PROTOCOL_GOOGLE_TALK)
   1370         // instead of "types" which we want to treate here (e.g. TYPE_HOME). So we don't add
   1371         // anything other than defaultType into allowedTypes and typeSpecificMapMax.
   1372         if (!Im.CONTENT_ITEM_TYPE.equals(newDataKind.mimeType) &&
   1373                 newDataKind.typeList != null && !newDataKind.typeList.isEmpty()) {
   1374             for (EditType editType : newDataKind.typeList) {
   1375                 allowedTypes.add(editType.rawValue);
   1376                 typeSpecificMaxMap.put(editType.rawValue, editType.specificMax);
   1377             }
   1378             if (defaultType == null) {
   1379                 defaultType = newDataKind.typeList.get(0).rawValue;
   1380             }
   1381         }
   1382 
   1383         if (defaultType == null) {
   1384             Log.w(TAG, "Default type isn't available for mimetype " + newDataKind.mimeType);
   1385         }
   1386 
   1387         final int typeOverallMax = newDataKind.typeOverallMax;
   1388 
   1389         // key: type, value: the number of current entries.
   1390         final SparseIntArray currentEntryCount = new SparseIntArray();
   1391         int totalCount = 0;
   1392 
   1393         for (ValuesDelta entry : mimeEntries) {
   1394             if (typeOverallMax != -1 && totalCount >= typeOverallMax) {
   1395                 break;
   1396             }
   1397 
   1398             final ContentValues values = entry.getAfter();
   1399             if (values == null) {
   1400                 continue;
   1401             }
   1402 
   1403             final Integer oldType = entry.getAsInteger(COLUMN_FOR_TYPE);
   1404             final Integer typeForNewAccount;
   1405             if (!allowedTypes.contains(oldType)) {
   1406                 // The new account doesn't support the type.
   1407                 if (defaultType != null) {
   1408                     typeForNewAccount = defaultType.intValue();
   1409                     values.put(COLUMN_FOR_TYPE, defaultType.intValue());
   1410                     if (oldType != null && oldType == TYPE_CUSTOM) {
   1411                         values.remove(COLUMN_FOR_LABEL);
   1412                     }
   1413                 } else {
   1414                     typeForNewAccount = null;
   1415                     values.remove(COLUMN_FOR_TYPE);
   1416                 }
   1417             } else {
   1418                 typeForNewAccount = oldType;
   1419             }
   1420             if (typeForNewAccount != null) {
   1421                 final int specificMax = typeSpecificMaxMap.get(typeForNewAccount, 0);
   1422                 if (specificMax >= 0) {
   1423                     final int currentCount = currentEntryCount.get(typeForNewAccount, 0);
   1424                     if (currentCount >= specificMax) {
   1425                         continue;
   1426                     }
   1427                     currentEntryCount.put(typeForNewAccount, currentCount + 1);
   1428                 }
   1429             }
   1430             newState.addEntry(ValuesDelta.fromAfter(values));
   1431             totalCount++;
   1432         }
   1433     }
   1434 }
   1435