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