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
17package com.android.contacts.model;
18
19import android.content.ContentValues;
20import android.content.Context;
21import android.database.Cursor;
22import android.net.Uri;
23import android.os.Bundle;
24import android.provider.ContactsContract;
25import android.provider.ContactsContract.CommonDataKinds.BaseTypes;
26import android.provider.ContactsContract.CommonDataKinds.Email;
27import android.provider.ContactsContract.CommonDataKinds.Event;
28import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
29import android.provider.ContactsContract.CommonDataKinds.Im;
30import android.provider.ContactsContract.CommonDataKinds.Nickname;
31import android.provider.ContactsContract.CommonDataKinds.Note;
32import android.provider.ContactsContract.CommonDataKinds.Organization;
33import android.provider.ContactsContract.CommonDataKinds.Phone;
34import android.provider.ContactsContract.CommonDataKinds.Photo;
35import android.provider.ContactsContract.CommonDataKinds.Relation;
36import android.provider.ContactsContract.CommonDataKinds.SipAddress;
37import android.provider.ContactsContract.CommonDataKinds.StructuredName;
38import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
39import android.provider.ContactsContract.CommonDataKinds.Website;
40import android.provider.ContactsContract.Data;
41import android.provider.ContactsContract.Intents;
42import android.provider.ContactsContract.Intents.Insert;
43import android.provider.ContactsContract.RawContacts;
44import android.text.TextUtils;
45import android.util.Log;
46import android.util.SparseArray;
47import android.util.SparseIntArray;
48
49import com.android.contacts.ContactsUtils;
50import com.android.contacts.model.account.AccountType;
51import com.android.contacts.model.account.AccountType.EditField;
52import com.android.contacts.model.account.AccountType.EditType;
53import com.android.contacts.model.account.AccountType.EventEditType;
54import com.android.contacts.model.account.GoogleAccountType;
55import com.android.contacts.model.dataitem.DataKind;
56import com.android.contacts.model.dataitem.PhoneDataItem;
57import com.android.contacts.model.dataitem.StructuredNameDataItem;
58import com.android.contacts.util.CommonDateUtils;
59import com.android.contacts.util.DateUtils;
60import com.android.contacts.util.NameConverter;
61
62import java.text.ParsePosition;
63import java.util.ArrayList;
64import java.util.Arrays;
65import java.util.Calendar;
66import java.util.Date;
67import java.util.HashSet;
68import java.util.Iterator;
69import java.util.List;
70import java.util.Locale;
71import java.util.Set;
72
73/**
74 * Helper methods for modifying an {@link RawContactDelta}, such as inserting
75 * new rows, or enforcing {@link AccountType}.
76 */
77public 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