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