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