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