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