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